SwiftUI JSONキーにハイフン(-)が付いている場合の対処 hyphen

hyphen json JSONEncoder

前回、SwiftJSONDecoderを使った処理方法を書きました。今回JSONキーにハイフンが混ざった処理をご紹介します。ハイフン(hyphen)が入った変数を作成することができません。ビルドエラーになってしまいます。しかしWeb上のAPIにはハイフンが混ざったキーもよく見かけます。例えば下記のようなJSONです。

{ "name-1": "Lenina Crowne" }

この場合”name1“なので下記のようにしたいのですがこれではビルドできません。

ビルドエラー
ビルドエラー

JSONキーにハイフンが含まれる場合CodingKeyが含まれたenum宣言を構造体に埋めてキー名の変換を行います。

struct TJson : Codable {
    var name1: String
    enum CodingKeys : String, CodingKey {
        case name1 = "name-1"
    }
}

enumの名称は「CodingKeys」とする必要があるようです。他の名前ではビルドは成功するものの実行するとエラーになりました。

struct TCodingKeyJSONView: View {
    @State var json1 = """
        { "name-1": "Lenina Crowne" }
    """
    var body: some View {
        Text(jsonConvert(json1).name1)
    }
    func jsonConvert(_ s1: String) -> TJson {
        do {
            return try JSONDecoder().decode(TJson.self, from: s1.data(using: .utf8)! )
        } catch {
            return TJson(name1: "")
        }
    }
}

ハイフンキーが入ったJSONの処理ができました。
ハイフンキーが入ったJSONの処理ができました。

JSONEncoderを使ってTJsonからJSON文字列に変換も試してみました。

struct TCodingKeyJSONView: View {
    var body: some View {
        Text(tJsonToJsonStr(TJson(name1: "Bernard Marx")))
    }
    func tJsonToJsonStr(_ j: TJson) -> String{
        let enc = JSONEncoder()
        do {
            let d1 = try enc.encode(j)
            return String(data: d1, encoding: .utf8)!
        } catch {
            return "{}"
        }
    }
}

JSONEncoderもうまく動きました。

ハイフンキー付きJSON作成する場合のJSONEncoder
ハイフンキー付きJSON作成する場合のJSONEncoder

firstIndexのIndex?to Int変換

utf16Offset

Swiftの文字列検索で使うfirstIndex()やlastIndex()はelement引数にCharacterすることでIndex?が返ります。この場合Int型ではなくIndexなので確認すると下のようなデータです。

Index(_rawBits: 65793)
Index(_rawBits: 65793)

Index型なので操作には問題ありませんが、Intに変換したい場合があります。この場合encodedOffsetを使うというサイトを見かけますが下記のようなメッセージがでます。

encodedOffset has been deprecated as most common usage is incorrect. Use utf16Offset(in:) to achieve the same behavior.
encodedOffsetは非推奨
encodedOffsetは非推奨

encodedOffsetは非推奨になったようでutf16Offset<S>(in s: S) -> Intをつかったほうがいいようです。たとえば下記のような”#”などをハッシュタグとして判断したい場合、何文字目に”#”が入っているのか確認できます。

let seven:String = "ウルトラ#セブン"
let i:Int = seven.firstIndex(of: "#")!.utf16Offset(in: seven)
print (i)

このコード内変数iには4が入ります。

SwiftUI ListとSection・データ更新について

SwiftUI-ListData

SwiftUIListは、データなどをリスト表示させる場合便利なコントロールです。このListSectionを持たせることができ ヘッダー・フッターをつけることもできます。LIstのシンプルな記述方法は下記です。

struct TListBox: View {
    var body: some View {
        List{
            Text("アイテム1")
            Text("アイテム2")
        }
    }
}
シンプルなList
シンプルなList

このコードの場合Textを個別に入れています。この記述方法ではList内のアイテムが多い場合対応できません。例えば100個のリストを作成する場合は下のように記述できます。

struct TListBox: View {
    var body: some View {
        List(0..<100){
            Text("アイテム\($0 + 1)")
        }
    }
}
100個のアイテムを作成
100個のアイテムを作成

Listのカッコ内に、Range式を入れることができます。Rangeは下記のように記述もできます。

var range: Range = 0..<10

RangeisEmpty, clamped, overlapsなどの機能も持っています。List内でForEachが記述できるのでRangeを使うことが可能です。

struct TListBox: View {
    var forEachText = ForEach(0..<10){
        Text("\($0 + 1)")
    }
    var body: some View {
        List{
            self.forEachText
        }
    }
}

ForEachforEachText変数に入れていますが、Listの中に直接書くこともできます。

Listに配列などが入れば便利なのですが、「List([1,2])」このようには記述できないようです。

Initializer 'init(_:rowContent:)' requires that 'Int' conform to 'Identifiable'

配列の場合、下のコードのように id指定が必要です。

List([1,2,3], id: \.self){
	Text("\($0)")
}

下のようなIdentifiable が付いた構造体の場合、配列をそのままList()に設定することが可能です。

struct TListItem: Identifiable {
    var id: Int
    init(_ i: Int) {
        id = i
    }
}

struct TListBox: View {
    var items: Array<TListItem> = [TListItem(1),TListItem(2)]
    var body: some View {
        List(items){
            Text("アイテム\($0.id)")
        }
    }
}

上のコードは、Identifiableを付けたTListItemの配列をList()内で使っています。

List内にはSectionを使いヘッダーとフッターをつけることもできます。

struct TListBox: View {
    var forEachText = ForEach(0..<2){
        Text("\($0 + 1)")
    }
    var body: some View {
        List{
            ForEach(0..<2){
                Section(header: Text("ヘッダー\($0 + 1)"), footer: Text("フッター\($0 + 1)")){
                    self.forEachText
                }
            }
        }
    }
}

List()のカッコ内に構造体配列の変数を設定することで動的にListを更新できます。下のコードは、Timerを使ってデータ内容を更新させListに反映させる例です。

struct TListItem: Identifiable {
    var id: Int
    var t: String
    init(_ i: Int, _ s: String) {
        id = i
        t = s
    }
}

struct TListBox: View {
    @State var listItems: [TListItem] = [];
    @State var count: Int = 0
    @State var timer1: Timer?
    var body: some View {
        List(listItems){
            Text($0.t)
        }.onAppear(){
            self.timer1?.invalidate()
            self.timer1 = Timer.scheduledTimer(withTimeInterval: 0.8, repeats: true) {_ in
                if (self.count <= 100) {
                    let df = DateFormatter()
                    df.dateFormat = "🕐 mm分ss秒SSS"
                    self.listItems.append(TListItem(self.count, df.string(from: Date())))
                    self.count += 1
                }
            }
        }
    }
}
タイマーを使ってList内データ更新
タイマーを使ってList内データ更新

上のように、List内アイテム間に線が入ってしまいます。これを回避するにはonAppear時に「separatorStyle = .none 」を設定します。

List(listItems){
	Text($0.t)
}.onAppear { UITableView.appearance().separatorStyle = .none }
.onDisappear { UITableView.appearance().separatorStyle = .singleLine }
Listアイテム間の線を消す
Listアイテム間の線を消す