Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// DNDClassOverviewRepository.swift
// munkit-example-core
//
// Created by Natalia Luzyanina on 17.04.2025.
//

import munkit
import Foundation

public actor DNDClassOverviewRepository {
private let networkService: MUNNetworkService<DNDAPITarget>

public let replica: any KeyedPhysicalReplica<String, DNDClassOverviewModel>

public init(networkService: MUNNetworkService<DNDAPITarget>) async {
self.networkService = networkService
self.replica = await ReplicaClient.shared.createKeyedReplica(
name: "DNDClassOverview",
childName: { name in "DNDClassOverview \(name)" },
fetcher: { index in try await networkService.executeRequest(target: .classOverview(index)) }
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public actor DNDClassesRepository {
public init(networkService: MUNNetworkService<DNDAPITarget>) async {
self.networkService = networkService
self.replica = await ReplicaClient.shared.createReplica(
name: "DndReplica",
name: "DNDClasses",
storage: nil,
fetcher: { try await networkService.executeRequest(target: .classes) }
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//
// DNDClassOverviewModel.swift
// munkit-example-core
//
// Created by Natalia Luzyanina on 17.04.2025.
//

import Foundation

public struct DNDClassOverviewModel: Decodable, Sendable {
public let name: String
public let hitDie: Int
public let savingThrows: [SavingThrow]
public let proficiencies: [Proficiency]
public let spellcasting: Spellcasting?

enum CodingKeys: String, CodingKey {
case name
case hitDie = "hit_die"
case savingThrows = "saving_throws"
case proficiencies
case spellcasting
}

public init(
name: String,
hitDie: Int,
savingThrows: [SavingThrow],
proficiencies: [Proficiency],
spellcasting: Spellcasting?
) {
self.name = name
self.hitDie = hitDie
self.savingThrows = savingThrows
self.proficiencies = proficiencies
self.spellcasting = spellcasting
}

public struct SavingThrow: Decodable, Sendable {
public let name: String
}

public struct Proficiency: Decodable, Sendable {
public let name: String
}

public struct Spellcasting: Decodable, Sendable {
public let info: [Info]

public struct Info: Decodable, Sendable {
public let desc: [String]
}
}
}
31 changes: 31 additions & 0 deletions Examples/munkit-example-ios/Source/Services/MobileService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// MobileService.swift
// exampleApp
//
// Created by Natalia Luzyanina on 18.04.2025.
//

import Foundation
import Moya
import munkit
import munkit_example_core

public actor MobileService {
public static let shared = MobileService()

public let networkService: MUNNetworkService<DNDAPITarget>

private init() {
let tokenProvider = TokenProvider()
let configuration = URLSessionConfiguration.default
configuration.headers = .default
configuration.urlCache = nil

let apiProvider = MoyaProvider<DNDAPITarget>(
session: Session(configuration: configuration, startRequestsImmediately: true),
plugins: [MUNLoggerPlugin.instance]
)

self.networkService = MUNNetworkService(apiProvider: apiProvider, tokenRefreshProvider: tokenProvider)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import UIKit

final class ClassOverviewController: HostingController<ClassOverviewView> {
init(viewModel: ClassOverviewViewModel) {
super.init(rootView: ClassOverviewView(viewModel: viewModel))

view.backgroundColor = .systemBackground
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import UIKit
import munkit_example_core
import munkit

enum ClassOverviewFactory {
@MainActor static func createClassOverviewController(id: String) async -> ClassOverviewController {
let repository = await DNDClassOverviewRepository(networkService: MobileService.shared.networkService)

let replica = await repository.replica.withKey(id)
let viewModel = ClassOverviewViewModel(
id: id,
replica: replica,
repository: repository
)
let controller = ClassOverviewController(viewModel: viewModel)

return controller
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import SwiftUI
import munkit_example_core

extension ClassOverviewView {
struct ViewItem {
let name: String
let hitDie: String
let savingThrows: [String]
let proficiencies: [String]
let description: String?
}
}

struct ClassOverviewView: View {
@ObservedObject var viewModel: ClassOverviewViewModel

var body: some View {
VStack(alignment: .leading, spacing: 16) {
if let viewItem = viewModel.viewItem {
Text(viewItem.name)
.font(.largeTitle)
.fontWeight(.bold)
.padding(.horizontal)
VStack(alignment: .leading, spacing: 12) {
Text("Hit Die: \(viewItem.hitDie)")
Text("Saving Throws: \(viewItem.savingThrows.joined(separator: ", "))")
Text("Proficiencies: \(viewItem.proficiencies.joined(separator: ", "))")
}
.padding()
.background(Color(.systemGray6))
.cornerRadius(12)
.padding(.horizontal)

if let description = viewItem.description {
ScrollView {
Text(description)
.font(.body)
.foregroundColor(.secondary)
}
.padding(.horizontal)
}
}

Spacer()
}
.onAppear { viewModel.startObserving() }
.onDisappear { viewModel.deinitObserver() }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import Foundation
import munkit
import munkit_example_core

final class ClassOverviewViewModel: ObservableObject {
@Published private(set) var viewItem: ClassOverviewView.ViewItem?

private let repository: DNDClassOverviewRepository
private let replica: any Replica<DNDClassOverviewModel>
private var observerTask: Task<Void, Never>?
private let observerStateStream: AsyncStreamBundle<Bool>

private let dndClassId: String

init(
id: String,
replica: any Replica<DNDClassOverviewModel>,
repository: DNDClassOverviewRepository
) {
self.dndClassId = id
self.replica = replica
self.repository = repository
self.observerStateStream = AsyncStream<Bool>.makeStream()
}

@MainActor
func startObserving() {
print("\(self): startObserving")

observerTask = Task { [weak self] in
guard let self else {
return
}

let observer = await replica.observe(activityStream: observerStateStream.stream)

self.observerStateStream.continuation.yield(true)

for await state in await observer.stateStream {
let model = state.data?.valueWithOptimisticUpdates

print("🐉 DNDClassOverviewViewModel: \(String(describing: model))")
guard let model else {
return
}
self.viewItem = .init(
name: model.name,
hitDie: "1d\(model.hitDie)",
savingThrows: model.savingThrows.map { $0.name },
proficiencies: model.proficiencies.map { $0.name },
description: model.spellcasting.map { $0.info.flatMap { $0.desc }.joined(separator: "\n") }
)
}
await observer.stopObserving()
}
}

func deinitObserver() {
observerStateStream.continuation.yield(false)
observerTask?.cancel()
observerTask = nil
}

deinit {
print("deinit ClassOverviewViewModel")
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
final class DndClassesCoordinator {
weak var router: NavigationRouter?

@MainActor func showClassOverview(for id: String) {}
@MainActor func showClassOverview(for id: String) {
Task {
let controller = await ClassOverviewFactory.createClassOverviewController(id: id)

router?.push(controller: controller, isAnimated: true)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import UIKit
import munkit
import munkit_example_core

enum DndClassesFactory {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ final class DndClassesViewModel: ObservableObject {
private let coordinator: DndClassesCoordinator
private let repository: DNDClassesRepository
private let replica: any Replica<DNDClassesListModel>
private let observerStateStream: AsyncStream<Bool>
private let observerContinuation: AsyncStream<Bool>.Continuation
private let observerStateStream: AsyncStreamBundle<Bool>
private var observerTask: Task<Void, Never>?

init(
Expand All @@ -20,11 +19,7 @@ final class DndClassesViewModel: ObservableObject {
self.coordinator = coordinator
self.repository = repository
self.replica = replica

let (observerActive, observerContinuation) = AsyncStream<Bool>.makeStream()

self.observerStateStream = observerActive
self.observerContinuation = observerContinuation
self.observerStateStream = AsyncStream<Bool>.makeStream()
}

@MainActor
Expand Down Expand Up @@ -90,10 +85,10 @@ final class DndClassesViewModel: ObservableObject {
return
}

let observer = await replica.observe(activityStream: observerStateStream)
let observer = await replica.observe(activityStream: observerStateStream.stream)

observerStateStream.continuation.yield(true)

self.observerContinuation.yield(true)

for await state in await observer.stateStream {
let viewItems = state.data?.valueWithOptimisticUpdates.results.map {
DndClassesView.ViewItem(id: $0.index, name: $0.name, isLiked: $0.isLiked)
Expand Down Expand Up @@ -129,7 +124,7 @@ final class DndClassesViewModel: ObservableObject {
}

func deinitObserver() {
observerContinuation.yield(false)
observerStateStream.continuation.yield(false)
observerTask?.cancel()
observerTask = nil
}
Expand Down
Loading