【Swift】この ModelActor ってなぜ生きてるの?

公式サンプルコードを見ながら作りました。



👉 【SwiftUI】SwiftData でスレッドセーフにバックグラウンドでデータを扱う - @ModelActor hatena-bookmark

🔄

サンプルコードに従った流れで実装しました。

ModelActor の内部 に static な自己インスタンスを持って、


@ModelActor
actor BirdBrain {
    private(set) static var shared: BirdBrain!
    
    static func createSharedInstance(modelContext: ModelContext) {
        shared = BirdBrain(modelContainer: modelContext.container)
    }
    
    func process(transaction verificationResult: VerificationResult<Transaction>) async {
    }
    
    func status(for statuses: [Product.SubscriptionInfo.Status], ids: PassIdentifiers) -> PassStatus {
    }
    
    func checkForUnfinishedTransactions() async {
    }
    
    func observeTransactionUpdates() {
    }

View の onAppear() でインスタンス生成。ModelActor 内の関数は Task 内で呼ぶ。


struct BackyardBirdsShopViewModifier: ViewModifier {
    @Environment(\.modelContext) private var modelContext
    
    func body(content: Content) -> some View {
        ZStack {
            content
        }
        .subscriptionPassStatusTask()
        .onAppear {
            BirdBrain.createSharedInstance(modelContext: modelContext)
        }
        .task {
            await BirdBrain.shared.observeTransactionUpdates()
            await BirdBrain.shared.checkForUnfinishedTransactions()
        }

👉 sample-backyard-birds/Multiplatform/Shop/BirdBrain.swift at 832515e4abb9224c1970e40a3bd9b82900019187 · apple/sample-backyard-birds hatena-bookmark
👉 sample-backyard-birds/Multiplatform/Shop/BackyardBirdsShopViewModifier.swift at main · apple/sample-backyard-birds hatena-bookmark

というかんじで、

  • ModelActor の内部 に static な自己インスタンスを持つ。
  • View の onAppear() でインスタンス生成。
  • ModelActor 内の関数は Task 内で呼ぶ。

このきまりで実装すると動くっちゃあ動くけど。

しかし、この ModelActor ってなぜ生きてるの?

いつ死ぬの?

死んでるように見えるけど、なぜ関数が呼べるのですか。

View と一緒に死ぬ?

(→ つづく)


【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)

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


SwiftUI + SwiftData で ToDo リスト を作ってみる

SwiftUI、SwiftData の公式チュートリアルってすごく良いですね

参考にしながら、まずは、Todo リスト的なものを作ってみました。

Landmarks というサンプルコードのタブに追加してみました。

import Foundation
import SwiftData
@Model
final class Todo {
var text: String
var date: Date
init(text: String = "", date: Date = Date()) {
self.text = text
self.date = date
}
}
extension Date {
func string(format: String = "yyyy-MM-dd HH:mm:ss") -> String {
let formatter = DateFormatter()
formatter.dateFormat = format
return formatter.string(from: self)
}
}
import SwiftUI
import SwiftData
struct TodoList: View {
@Environment(\.modelContext) private var context
@Query(sort: \Todo.date) private var todos: [Todo]
@State private var selectedID: PersistentIdentifier?
@State private var bottomID: PersistentIdentifier?
@State private var text = ""
@FocusState var focused: Bool
var body: some View {
NavigationStack {
VStack(spacing: 0) {
ScrollView {
ForEach(todos) { todo in
LazyVStack(alignment: .leading) {
Text(todo.text)
.font(.headline)
Text(todo.date.string())
.font(.caption)
.foregroundStyle(.secondary)
}
.padding()
.foregroundStyle(selectedID == todo.id ? .white : .black)
.background(selectedID == todo.id ? .blue : .white)
.onTapGesture {
withAnimation {
if selectedID == todo.id {
clear()
} else {
selectedID = todo.id
text = todo.text
}
}
}
}
.padding()
.scrollTargetLayout()
}
.scrollPosition(id: $bottomID)
.onAppear {
withAnimation {
// for todo in todos {
// delete(todo: todo)
// }
// for i in 1 ..< 10000 {
// insert(text: "\(i) どうするのこれ")
// }
bottomID = todos.last?.id
}
}
HStack {
TextField("", text: $text)
.font(.system(.title3))
.textFieldStyle(.roundedBorder)
.focused($focused)
Button {
withAnimation {
if selectedID != nil {
update(todo: selectedTodo, text: text)
} else {
insert(text: text)
}
}
} label: {
Image(systemName: selectedID != nil ? "arrow.clockwise" : "plus")
.frame(height: 25)
}
.buttonStyle(.borderedProminent)
if selectedID != nil {
Button {
withAnimation {
delete(todo: selectedTodo)
}
} label: {
Image(systemName: "xmark")
.frame(height: 25)
}
.buttonStyle(.borderedProminent)
}
}
.padding()
}
.navigationTitle("Todo List")
}
}
private func insert(text: String) {
context.insert(Todo(text: text))
clear()
self.bottomID = todos.last?.id
}
private func delete(todo: Todo) {
context.delete(todo)
clear()
}
private func update(todo: Todo, text: String) {
delete(todo: todo)
insert(text: text)
clear()
}
private func clear() {
self.focused = false
self.text = ""
self.selectedID = nil
}
private var selectedTodo: Todo {
return todos.first(where: { todo in todo.id == selectedID })!
}
}
#Preview {
TodoList()
.modelContainer(for: Todo.self)
.frame(width: 300)
}

Android の「Jetpack Compose + ストレージ的な何か」で作るより、簡単に直感的に作れるような気がしています。

もっと上手にプログラミングできるよう日々精進していこうと思います。