- 記事一覧 >
- ブログ記事
SwiftUI BOM付UTF-8のCSVファイルを出力して外部に保存
はじめに
Swift(SwiftUI) で iPhone, iPad から BOM 付 UTF-8 のカンマ区切り CSV ファイルを保存できるアプリを作成しました。
アプリと言っても、ボタンが一つだけ有り、ダミーデータから CSV ファイルを出力するという最低限のものです。
今回、実装内容と速攻で実装して動作確認する手順を書きます。
【検証環境】
macOS Monterey 12.6
Xcode 13.4.1
Swift 5.7 (SwiftUI)
本記事の内容により何らかの不都合が生じても当方は一切責任を負いません。
動作内容
達成すべきことは、
・iPhone のテキストビューワー(ファイルアプリでタップして表示されるもの)で表示できる
・Windows のエクセル(バージョン 2209)で表示できる
・macOS のエクセルで表示できる
のみとします。
時刻 | 名前 | 数値 |
---|---|---|
1年くらいランダムに遡った日時 | 太郎[1〜10 の連番] | 0 ~ 9999 の適当な数 |
を
10 行生成します。
これをカンマ区切り CSV 化し、共有します。(シェアシートを表示します。)
動作内容は、以下です。
↑
この例では、単純に本体に保存していますが、Teams のチャットルームにアップできたりします。
実装内容
詳細は、下に掲載のソースコード中のコメントを参照してください。
主に、以下の部分がキモになります。
・"[文字列]"
の [文字列] 部分に "
が有る場合、""
とする。
・エクセルに表示したときに文字化けするため、BOM 付 UTF-8 とする。
【BOM 無し UTF-8 の場合】
Windows、macOS のエクセルで文字化けします。iPhone のテキストビューワーや macOS のテキストエディットでは文字化けしません。
シュミレーターで保存したファイル(
data.csv
)は、/Users/<ユーザー名>/Library/Developer/CoreSimulator/Devices/<デバイスID>/data/Containers/Shared/AppGroup/<アプリID>/File Provider Storage
から取り出せます。ターミナルで、find Library/Developer/CoreSimulator/Devices -name data.csv
などで見つけられます。
ソースコード全体像です。
一応、実装手順
セクションで説明しますが、新規プロジェクトを作成後、ContentView.swift
しか変更しません。
import SwiftUI
struct MyData {
var timestamp: Date
var name: String
var number: Int
}
struct ContentView: View {
var body: some View {
Button(action: {
var csv = ""
do {
for i in 1...10 {
// ダミーデータ作成
let randomNumber = Int.random(in:0...9999)
var goBackTime = 0
goBackTime = goBackTime + (Int.random(in:0..<365) * 24 * 60 * 60)
goBackTime = goBackTime + (Int.random(in:0..<24) * 60 * 60)
goBackTime = goBackTime + (Int.random(in:0..<60) * 60)
goBackTime = goBackTime + Int.random(in:0..<60)
let setTime = Calendar.current.date(byAdding: .second, value: ( goBackTime * -1 ) , to: Date())!
let myData = MyData(timestamp: setTime, name:"太郎" + String(i), number: randomNumber)
// myData は、
// timestamp: 1年くらいランダムに遡った日時
// name: 太郎[1〜10の連番]
// number: 0〜9999の適当な数
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"// 日時の表示フォーマットを設定
var line = ""
line += formatter.string(from: myData.timestamp) + "," // [日時フォーマット結果],
line += "\"" + ((myData.name).replacingOccurrences(of: "\"", with: "\"\"") as String) + "\","
// "[名前]", の[名前]に " が含まれていたら、" を "" に置換(今回は必ず含まれないが)
line += String(myData.number) + "\r\n" // [数字]¥r¥n
csv = line + csv //csv = CSVとして出力する内容全体
}
}
csv = "時刻,名前,数値\r\n" + csv // 見出し行を先頭行に追加
let tmpFile: URL = URL(fileURLWithPath: "data.csv", relativeTo: FileManager.default.temporaryDirectory)
// テンポラリディレクトリ/data.csv の URL (ファイルパス)取得
if let strm = OutputStream(url: tmpFile, append: false) {// 新規書き込みでストリーム作成
// if let ... により、strm が nil ではないとき実行
strm.open() // ストリームオープン(fopenみたいな)
let BOM = "\u{feff}"
// U+FEFF:バイトオーダーマーク(Byte Order Mark, BOM)
// Unicode の U+FEFFは、表示がない文字。「ZERO WIDTH NO-BREAK SPACE」(幅の無い改行しない空白)
strm.write(BOM, maxLength: 3)// UTF-8 の BOM 3バイト 0xEF 0xBB 0xBF 書き込み
let data = csv.data(using: .utf8)
// string.data(using: .utf8)メソッドで文字コード UTF-8 の
// Data 構造体を得る
_ = data?.withUnsafeBytes {//dataのバッファに直接アクセス
strm.write($0.baseAddress!, maxLength: Int(data?.count ?? 0))
// 【$0】
// 連続したメモリ領域を指す UnsafeRawBufferPointer パラメーター
// 【$0.baseAddress】
// バッファへの最初のバイトへのポインタ
// 【maxLength:】
// 書き込むバイトdataバッファのバイト数(全長)
// 【data?.count ?? 0】
// ?? は、Nil結合演算子(Nil-Coalescing Operator)。
// data?.count が nil の場合、0。
// 【_ = data】
// 戻り値を利用しないため、_で受け取る。
}
strm.close() // ストリームクローズ
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.001) {//念の為若干遅らせて、0.001秒後に実行(意味無いかも)
// DispatchQueue.main.asyncAfter(deadline: .now() + 秒) { 遅延実行したい処理 }
shareApp(shareText: csv, shareLink: tmpFile.absoluteURL )
// tmpFile.absoluteURL = テンポラリファイルの絶対パス(URL型)
}
}) {
Image(systemName:"square.and.arrow.up")
.resizable()
.scaledToFit()
.frame(width: 100, height: 100)
}
}
}
private func shareApp(shareText: String, shareLink: URL ) {
let items = [shareLink] as [Any]// shareLink = テンポラリファイルの絶対パス(URL型)
let activityVC = UIActivityViewController(activityItems: items, applicationActivities: nil)
// UIActivityViewController:共有の時に画面外からフェードインしてくる画面。シェアシート。
// activityItems:UIImage(画像)、URL(URL)、String(テキスト)など保存したいものを指定
// applicationActivities:シェアシートをカスタマイズしたい時に使う。デフォルトで良い場合は、nil。
if UIDevice.current.userInterfaceIdiom == .pad {// デバイスがiPadだったら
let deviceSize = UIScreen.main.bounds// 画面サイズ取得
if let popPC = activityVC.popoverPresentationController {
// ポップオーバーの設定
// iPadの場合、sourceView、sourceRectを指定しないとクラッシュする。
popPC.sourceView = activityVC.view // sourceRectの基準になるView
popPC.barButtonItem = .none// ボタンの位置起点ではない
popPC.sourceRect = CGRect(x:deviceSize.size.width/2, y: deviceSize.size.height, width: 0, height: 0)// Popover表示起点
}
}
let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
// UIWindowScene のインスタンス作成
// 【UIApplication】
// アプリケーションを制御、管理するクラス
// 【.shared】
// シングルトンのUIApplicationインスタンスにアクセス
// 【.connectedScenes】
// アクティブになっているシーンにアクセス(Set<UIScene>)
// 【.first as? UIWindowScene】
// マルチウィンドウでは無いため、単純に一番最初の要素にアクセスして、UIWindowScene にキャスト
let rootVC = windowScene?.windows.first?.rootViewController
// rootVC = rootViewController:アプリ初期画面(大元のViewController)
rootVC?.present(activityVC, animated: true,completion: {})
// アニメーション有りでシェアシート(activityVC)表示(present)
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
実装手順
Xcode を起動して、Create a new Xcode project
をクリックします。
Xcode を起動して、App
を選択して、Next
をクリックします。
Project Name: [任意]
Team: [任意]
Organization Identifier: [任意]
Bundle Identifier: [任意]
Interface: SwiftUI
Language: Swift
Use Core Data: チェック無し
Include Tests: チェック無し
とし、Next
をクリックします。
ContentView.swift
の内容をそっくりそのまま上記の内容に差し替えます。
シュミレーターで起動してみます。(動画は、iPhone12
のシュミレーターです。)
ヨシ!
その他、宣伝、誹謗中傷等、当方が不適切と判断した書き込みは、理由の如何を問わず、投稿者に断りなく削除します。
書き込み内容について、一切の責任を負いません。
このコメント機能は、予告無く廃止する可能性があります。ご了承ください。
コメントの削除をご依頼の場合はTwitterのDM等でご連絡ください。