-
How can I use Boutique with a parent child relationship? For instance, I have a list of books and each book has an author. I've tried setting up a bookController and authorController but on the BookDetailView I would like to link to the AuthorDetailView by displaying the Author's name and a NavigationLink to the detail view. Are these types of associations possible using Boutique? |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 1 reply
-
Apologies for the long reply time @natebird, I've been dealing with some health issues and family matters so open source has taken a bit of a back seat over the last month or two. I'll probably be a bit slow to reply for a bit, but I wanted to provide you an answer that may be helpful, with some code and a real world scenario from one of my apps. Imagine you have two models, a
The models for public struct RichLink: Codable, Equatable, Identifiable {
public let id: UUID
public let createdAt: Date
public var updatedAt: Date
public let isSynchronized: Bool
public let originalURL: URL
public let resolvedURL: URL
public let title: String?
public let description: String?
public var tags: [Tag]
} public struct Tag: Codable, Hashable, Identifiable {
public var id: UUID
public var createdAt: Date
public var updatedAt: Date
public var title: String
public var iconName: String
public var hexColor: String
} Now we'll create a public final class RichLinksController: ObservableObject {
private let tagsController: TagsController
@Stored public var links: [RichLink]
public init(store: Store<RichLink>, appState: AppState) {
self._links = Stored(in: .richLinksStore)
self.tagsController = TagsController(appState: appState)
}
...
// MARK: Storage
@MainActor
func storeLink(_ richLink: RichLink) async throws {
try await self.$links.insert(richLink)
try await self.updateTagsState(fromLink: richLink)
}
// MARK: Removal
@MainActor
func remove(link: RichLink) async throws {
try await self.$links.remove(link)
}
@MainActor
func removeAll() async throws {
try await self.$links.removeAll()
}
...
func updateTagsState(fromLink link: RichLink) async throws {
try await self.addTags(fromLink: link)
}
func addTags(fromLink link: RichLink) async throws {
let linkTags = Set(link.tags)
let allTags = await Set(self.tagsController.tags)
let newTags = linkTags.subtracting(allTags)
for tag in newTags {
try await self.tagsController.insert(tag: tag)
}
}
...
} For the purposes of discussing the relationship between links and tags, let's zoom in on the extension RichLinksController {
@MainActor
func addTags(_ tags: [Tag], forLink link: RichLink) async throws {
guard var matchingLink = self.links.first(where: { $0.id == link.id }) else { return }
matchingLink.tags.append(contentsOf: tags)
try await self.$links.insert(matchingLink)
}
@MainActor
func setTags(_ tags: [Tag], forLink link: RichLink) async throws {
guard var matchingLink = self.links.first(where: { $0.id == link.id }) else { return }
matchingLink.tags = tags
try await self.$links.insert(matchingLink)
}
@MainActor
func removeTags(_ tags: [Tag], forLink link: RichLink) async throws {
guard var matchingLink = self.links.first(where: { $0.id == link.id }) else { return }
for tag in tags {
matchingLink.tags.removeAll(where: { $0.id == tag.id })
}
try await self.$links.insert(matchingLink)
}
@MainActor
func removeTagFromAllLinks(_ tag: Tag) async throws {
var linksToUpdate: [RichLink] = []
for var link in self.links {
link.tags.removeAll(where: { $0 == tag })
linksToUpdate.append(link)
}
try await self.$links.insert(linksToUpdate)
}
@MainActor
func updateTagForExistingLinks(initialTag: Tag, updatedTag: Tag) async throws {
var replacementTag = updatedTag
replacementTag.id = initialTag.id
var linksWithTagUpdates: [RichLink] = []
let linksWithMatchingTag = self.links.filter({ $0.tags.contains(initialTag) })
for var link in linksWithMatchingTag {
link.tags.removeAll(where: { $0.id == initialTag.id })
link.tags.append(replacementTag)
linksWithTagUpdates.append(link)
}
try await self.$links.insert(linksWithTagUpdates)
}
} If you look at the code interacts with tags it's pretty obvious that this code can be improved. There's a lot of boilerplate, we're duplicating logic, we have to be careful and make sure we manually synchronize any tags we add to links, but it does work. More importantly demonstrates a path to standardizing this behavior and making it automatic, or as close to automatic as we can make it. My ideal implementation is to leverage static typing to build relationships between two types ( // Before
func addTags(_ tags: [Tag], forLink link: RichLink)
// After
func add<Parent, Child>(_ values: [Child], to: Parent, keyPath: KeyPath<Parent, [Child]>) There's only one downside to this idea, the fact that I haven't had the chance to implement it. I'm unfortunately very strapped for time and energy these days, but I would be very excited to see this functionality available in Boutique. If you or anyone else have any interest in adding this to Boutique, I'd be happy to provide some help with code reviews and guidance. Hope that answers your question, if you have any more thoughts or questions about anything I said above please don't hesitate to ask, though it may take me a little bit to respond. And thank you for asking about this in the first place, would love to see if I can make Boutique work for you! |
Beta Was this translation helpful? Give feedback.
Apologies for the long reply time @natebird, I've been dealing with some health issues and family matters so open source has taken a bit of a back seat over the last month or two. I'll probably be a bit slow to reply for a bit, but I wanted to provide you an answer that may be helpful, with some code and a real world scenario from one of my apps.
Imagine you have two models, a
Tag
and aRichLink
(basically a link but with additional metadata).The models for
Tag
andRichLink
look like this.