【Swift】モデルクラスの ID に UUID() を使ってたらヤバい - PersistentIdentifier

すいません、別にヤバくはないです。

みなさんはどう書いてますか。

私はこんな感じで書いてました。


final class Card {
  var id: UUID
  var front: String
  var back: String

  init(front: String, back: String) {
    self.id = UUID()
    self.front = front
    self.back = back
  }
}

これを、Apple 公式サンプルではこんなかんじで書いてました。


final class Card {
  var front: String
  var back: String

  init(front: String, back: String) {
    self.front = front
    self.back = back
  }
}

extension Card: Identifiable { }

ID (id) がない!

Playground で見てみます。


final class Card {
  var front: String
  var back: String

  init(front: String, back: String) {
    self.front = front
    self.back = back
  }
}

extension Card: Identifiable { }

let card = Card(front: "前", back: "後")

print(card.id)
print(card.front)
print(card.back)

// ObjectIdentifier(0x0000600000c014d0)
// 前
// 後

id が取れます。

Identifiable を継承しても同様です。

Apple のサンプルコードは少し省略して書きましたが、実際は以下のようなコードです。


final class Card: ObservableObject {
  @Published var front: String
  @Published var back: String
  var creationDate: Date

  init(front: String, back: String, creationDate: Date = .now) {
    self.front = front
    self.back = back
    self.creationDate = creationDate
  }
}

extension Card: Identifiable { }


struct CardCarousel: View {
  @State private var selectedCardID: Card.ID?  // *
  @FocusState private var focusCardID: Card.ID?
  private let initialCardID: Card.ID

  let editing: Bool
  var cards: [Card]

  init(editing: Bool, cards: [Card], selectedCard: Card) {
    self.editing = editing
    self.cards = cards
    initialCardID = selectedCard.id
  }

  var body: some View {
    VStack {
      ScrollView(.horizontal) {
        LazyHStack(spacing: 0) {
         ForEach(cards) { card in // *
           Group {
             if editing {
               CardEditorView(card: card)
             } else {
               FlashCardView(card: card)
                 .id(card.id)

ObjectIdentifier(0x0000600000c014d0) の型は、

Card.ID

です。

また、List 内の ForEach() の引数は一つです。

便利ですよね!

 

🆔 SwiftData の場合

同じく Apple サンプルコードです。

SwiftData では更に便利になっています。

こちらにもモデルクラスに ID はありません。

Identifiable もありません。


@Model
final class Card {
  var front: String
  var back: String
  var creationDate: Date

  init(front: String, back: String, creationDate: Date = .now) {
    self.front = front
    self.back = back
    self.creationDate = creationDate
  }
}


struct CardCarousel: View {
  @State private var selectedCardID: PersistentIdentifier? // *
  @FocusState private var focusCardID: PersistentIdentifier?
  private let initialCardID: PersistentIdentifier

  let editing: Bool
  var cards: [Card]

  init(editing: Bool, cards: [Card], selectedCard: Card) {
    self.editing = editing
    self.cards = cards
    initialCardID = selectedCard.id
  }

  var body: some View {
    VStack {
      ScrollView(.horizontal) {
        LazyHStack(spacing: 0) {
          ForEach(cards) { card in // *
            Group {
              if editing {
                CardEditorView(card: card)
              } else {
                FlashCardView(card: card)
                  .id(card.id)

ID の型は、PersistentIdentifier です。

書き方は違いますが、同様にモデルクラスにIDなしで使えます。

 

🆔 まとめ

今回勉強した内容で、以前の Todo アプリを書き換えます。

👉 SwiftUI + SwiftData で ToDo リスト を作ってみる hatena-bookmark

少しだけスッキリしました。


【Swift】Playground で API から JSON データを取得して デコード して画像を表示する 🐶

Xcode Playground 便利です。

ちょっとしたコード記述の確認に使えます。

今回は、通信処理まわりのコードをやってみました。

Dog API というのを利用しました。


https://dog.ceo/api/breeds/image/random


{
    "message": "https://images.dog.ceo/breeds/chow/n02112137_1015.jpg",
    "status": "success"
}

👉 Dog API hatena-bookmark

 

🐶 JSON の取得


let url = "https://dog.ceo/api/breeds/image/random"
let session = URLSession.shared
guard let (data, response) = try? await session.data(from: URL(string: url)!),
      let httpResponse = response as? HTTPURLResponse,
      httpResponse.statusCode == 200
else {
  throw // ...
}

print(String(data: data, encoding: .utf8)!)
// {"message":"https:\/\/images.dog.ceo\/breeds\/lhasa\/n02098413_5594.jpg","status":"success"}

 

🐶 JSON デコード


struct Dog: Decodable {
  let message: String
  let status: String
}


let data: Data = // ...
do {
  let decoder = JSONDecoder()
  decoder.dateDecodingStrategy = .millisecondsSince1970
  print(try decoder.decode(Dog.self, from: data))
} catch {
  throw // ...
}

// Dog(message: "https://images.dog.ceo/breeds/lhasa/n02098413_5594.jpg", status: "success")

 

🐶 WebKit で表示


let url = "https://images.dog.ceo/breeds/lhasa/n02098413_5594.jpg"
let view = WKWebView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
let request = URLRequest(url: URL(string: url)!)
view.load(request)
PlaygroundPage.current.liveView = view

 

🐶 まとめ

以上をまとめたコードを貼っておきます。

Xcode Playground にベタ貼りするだけで動きます。

実行時にアレなので強引にメインスレッドに揃えています。


Task { @MainActor in
  // ...
}

前回に調べた内容を参考にしました。

👉 【Swift】URLSession.shared.dataTask() をうまく使いこなせない hatena-bookmark

よろしくおねがいします。

次回は、Concurrency をやってみたいと思います。


【Swift】URLSession.shared.dataTask() をうまく使いこなせない

これは、犬の写真を取得する既存のコードです。

URLSessionで completionHandlerベースの便利なメソッドを使用しています。

コードは簡単なように見えて、私のテストでうまくいきましたが、少なくとも3つの間違いがあります。流れ順に見てみましょう。

dataTask を作成して resume します。そして、タスクが完了したら、completionHandler で応答を確認し、画像を作成し終了します。前後しながら流れていきます。

スレッドはどうでしょう。

小さなコードなのに驚くほど複雑で合計で3つの異なる実行コンテキストがあります。

最も外側のレイヤーは呼び出し元のスレッドまたはキューで実行され、completionHandler はセッションのデリゲートキューで実行され、最後に completionHandler はメインキューで実行されます。コンパイラでは捕捉できないので、スレッドの問題を避けるために細心の注意を払う必要があります。

今気づきましたが、completionHandler の呼び出しは、メインキューに一貫してディスパッチされません。これはバグかもしれません。

また、早期リターンをしていないのでエラーが発生した場合、completionHandler を2回呼び出すことになります。これは、作成者の意図と違う可能性があります。

また、最後の UIImage の作成は失敗する可能性があります。データが誤った形式の場合、この UIImage は nil を返すので、nil 画像と nil エラーの両方で completionHandler を呼び出すでしょう。

 

💡 新しい API


URLSession.shared.data()
URLSession.shared.upload()
URLSession.shared.download()
URLSession.shared.bytes()

👉 URLSession | Apple Developer Documentation hatena-bookmark

以下、参考にしたい新しいAPIのサンプルコードです。


// Fetch photo with async/await

func fetchPhoto(url: URL) async throws -> UIImage {
  let (data, response) = try await URLSession.shared.data(from: url)

  guard let httpResponse = response as? HTTPURLResponse,
        httpResponse.statusCode == 200 else {
    throw WoofError.invalidServerResponse
  }

  guard let image = UIImage(data: data) else {
    throw WoofError.unsupportedImage
  }
  return image
}


// URLSession.data

let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
      httpResponse.statusCode == 200 
else {
  throw MyNetworkingError.invalidServerResponse
}


// URLSession.upload

var request = URLRequest(url: url)
request.httpMethod = "POST"

let (data, response) = try await URLSession.shared.upload(for: request, fromFile: fileURL)
guard let httpResponse = response as? HTTPURLResponse,
      httpResponse.statusCode == 201
else {
  throw MyNetworkingError.invalidServerResponse
}


// URLSession.download

let (location, response) = try await URLSession.shared.download(from: url)
guard let httpResponse = response as? HTTPURLResponse,
      httpResponse.statusCode == 200 
else {
  throw MyNetworkingError.invalidServerResponse
}

try FileManager.default.moveItem(at: location, to: newLocation)


// Cancellation

let task = Task {
  let (data1, response1) = try await URLSession.shared.data(from: url1)
  let (data2, response2) = try await URLSession.shared.data(from: url2)
}

task.cancel()


// asyncSequence demo

let (bytes, response) = try await URLSession.shared.bytes(from: Self.eventStreamURL)
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
  throw WoofError.invalidServerResponse
}

for try await line in bytes.lines {
  let photoMetadata = try JSONDecoder().decode(PhotoMetadata.self, from: Data(line.utf8))
  await updateFavoriteCount(with: photoMetadata)
}


// task specific delegate demo

class AuthenticationDelegate: NSObject, URLSessionTaskDelegate {
  private let signInController: SignInController
    
  init(signInController: SignInController) {
    self.signInController = signInController
  }
    
  func urlSession(_ session: URLSession,
                  task: URLSessionTask,
                  didReceive challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
    if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic {
      do {
        let (username, password) = try await signInController.promptForCredential()
          return (.useCredential, URLCredential(user: username, password: password, persistence: .forSession))
      } catch {
        return (.cancelAuthenticationChallenge, nil)
      }
    } else {
      return (.performDefaultHandling, nil)
    }
  }
}

最初の async/await のコードをベースに、どれかのパターンで対応できそうです。

確かに使いやすそうです。

 

💡 まとめ

最近のプログラミング言語は仕様の変化が速いので、ネットで検索するとどれを使ったらいいのか、私たち初心者は混乱します。

ありがたい Apple 公式の公開資料からでした。

👉 Use async/await with URLSession - WWDC21 - Videos - Apple Developer hatena-bookmark

次は、人気の

Alamofire/Alamofire: Elegant HTTP Networking in Swift hatena-bookmark

を使ってみたいと思っています。


// Automatic String to URL conversion, Swift concurrency support, and automatic retry.
let response = await AF.request("https://httpbin.org/get", interceptor: .retryPolicy)
                       // Automatic HTTP Basic Auth.
                       .authenticate(username: "user", password: "pass")
                       // Caching customization.
                       .cacheResponse(using: .cache)
                       // Redirect customization.
                       .redirect(using: .follow)
                       // Validate response code and Content-Type.
                       .validate()
                       // Produce a cURL command for the request.
                       .cURLDescription { description in
                         print(description)
                       }
                       // Automatic Decodable support with background parsing.
                       .serializingDecodable(DecodableType.self)
                       // Await the full response with metrics and a parsed body.
                       .response
// Detailed response description for easy debugging.
debugPrint(response)

どうぞよろしくおねがいします。