본문 바로가기
이모저모/SwiftUI

Using @globalActor RealmSwift in TCA

by ARpple 2024. 3. 25.

1. Realm 전용 Actor 지정하기

@globalActor

 

GlobalActor | Apple Developer Documentation

A type that represents a globally-unique actor that can be used to isolate various declarations anywhere in the program.

developer.apple.com

@MainActor가 메인 스레드에서 작동하는 것과 같이 특정 Actor에서의 쓰레드 동작을 보장하는 방법

 

 

[SwiftUI] @globalActor

How to use Global Actors in Swift (@globalActor) | Swift Concurrency MainActor의 작동이 메인 스레드를 통해 이루어지는 게 보장(싱글턴)되는 것과 마찬가지로 특정 액터 사용을 글로벌 스레드를 통해 사용할 수

velog.io

@globalActor actor DBActor: GlobalActor {
    static var shared = DBActor()
}
@DBActor protocol RealmAPIs{ }
💡 위 코드에서 프로토콜은 DBActor라는 특정 Actor에 고립(isolated)되는 특성을 갖게된다.

2. Realm을 접근하기 위한 dependency 만들기

1. DBActor에서 Realm 인스턴스 생성하기

@DBActor protocol RealmAPIs{
    func initRealm() async throws
    func appendShoppingList(_ shoppingList: ShoppingListTable)
    func getShoppingLists() -> [ShoppingListTable]
}

2. 프로토콜 기반 RealmAPI 접근 class 생성

@DBActor final class RealmAPIsClient: RealmAPIs{
    var realm:Realm!
    init(){ }
    // 이 메서드는 TCA Dependency 인스턴스로 처음 접근할 때 꼭 실행해야한다.
    func initRealm() async throws{
        realm = try await Realm(actor: DBActor.shared)
    }
    func appendShoppingList(_ shoppingList: ShoppingListTable){
        try? realm?.write({
            realm?.add(shoppingList, update: .modified)
        })
    }
    @DBActor func getShoppingLists() -> [ShoppingListTable] {
        Array(realm.objects(ShoppingListTable.self))
    }
}

3. DependencyKey 프로토콜 등록

@DBActor protocol RealmAPIs{
    func initRealm() async throws
    func appendShoppingList(_ shoppingList: ShoppingListTable)
    func getShoppingLists()  ->[ShoppingListTable]
}

4. DependencyValues 등록

extension DependencyValues {
    var dbAPIClients: RealmAPIs {
        get { self[RealmAPIsClientKey.self] }
        set { self[RealmAPIsClientKey.self] = newValue }
    }
}

2. Feature에서 선언하기

@Reducer struct AnalyzeFeature{
...
    @DBActor @Dependency(\.dbAPIClients) var apiClient
...
}

3. Feature에서 준수해야할 점

RealmDependency에 접근하는 경우 Task를 변경해야한다

방법 1. Global Task에서 접근 순간마다 async 처리하기
// RealmTable을 Struct로 변경...
struct ShoppingList:Identifiable,Equatable{
    ...
    @DBActor init(table: ShoppingListTable){
    	...
    }
}
// TCA 비동기 코드...
return .run { send in
  let li = await apiClient.getShoppingLists().asyncMap{ 
	  await ShoppingList(table: $0)
  }
  await send(.updateShoppingLists(li))
}
방법 2. Task Scope에 DBActor로 Actor를 지정한다.
return .run {@DBActor send in
  do{
    try await apiClient.initRealm()
    let li = apiClient.getShoppingLists().map{ShoppingList(table: $0)}
    await send(.updateShoppingLists(li))
    }catch{
    fatalError("get error!!")
  }
}

TCA의 IdentifiedArrayOf에 RealmTable 아이템은 접근할 수 없다…

💡 정확히는 RealmTable(Class)의 데이터를 MainActor로 보낼때 Realm에서 thread isolated 오류를 던지는 것이다.
⇒ RealmTable 데이터를 View, Feature에서 나타내는 Struct로 한 번 변환하기로 결정.
⇒ 다른 해결방법도 많이 있을것 같다. 하지만 데이터를 Struct로 관리하는게 추후 메모리 할당 관련 문제가 발생하지 않기 때문에 Struct로 변환하기로 결정했다.
  • 예시 코드
...
@DBActor @Dependency(\.dbAPIClients) var apiClient
var body: some ReducerOf<Self>{
    ...
    return .run {@DBActor send in
        do{
            try await apiClient.initRealm()
            let listTables = apiClient.getShoppingLists()
            let list = listTables.map{ShoppingList(table: $0)}
            await send(.updateShoppingLists(list))
        }catch{
            fatalError("get error!!")
        }
    }
    ...
}
...

댓글