diff --git a/Development/Demo/MyBook.swift b/Development/Demo/MyBook.swift index b9d8f24..9e48cfb 100644 --- a/Development/Demo/MyBook.swift +++ b/Development/Demo/MyBook.swift @@ -24,113 +24,117 @@ import StorybookKit import SwiftUISupport @MainActor -let myBook2 = BookContainer( - store: .init( - title: "MyUI", - links: { - - BookNavigationLink(title: "Preview UI") { - BookSection(title: "Section") { - - Text( - """ - Something description about this section. - """ - ) +let myBook2 = Book.init( + title: "MyUI", + folders: { - BookPreview { _ in - let view = UIView(frame: .init(x: 0, y: 0, width: 80, height: 80)) - view.backgroundColor = .systemPurple - NSLayoutConstraint.activate([ - view.widthAnchor.constraint(equalToConstant: 80), - view.heightAnchor.constraint(equalToConstant: 80), - ]) - return view - } - - BookPreview(title: "A component") { _ in - let view = UIView(frame: .null) - view.backgroundColor = .systemPurple - return view - } - .previewFrame(width: 80, height: 80) + BookFolder(title: "A") { + BookPageGroup(title: "Preview UI") { + BookNavigationLink(title: "Preview UI") { + BookSection(title: "Section") { - BookPreview { _ in - let view = UIView(frame: .init(x: 0, y: 0, width: 80, height: 80)) - view.backgroundColor = .systemPurple - return view - } - .previewFrame(maxWidth: .greatestFiniteMagnitude, idealHeight: 10) + Text( + """ + Something description about this section. + """ + ) - } + BookPreview { _ in + let view = UIView(frame: .init(x: 0, y: 0, width: 80, height: 80)) + view.backgroundColor = .systemPurple + NSLayoutConstraint.activate([ + view.widthAnchor.constraint(equalToConstant: 80), + view.heightAnchor.constraint(equalToConstant: 80), + ]) + return view + } - } + BookPreview(title: "A component") { _ in + let view = UIView(frame: .null) + view.backgroundColor = .systemPurple + return view + } + .previewFrame(width: 80, height: 80) - BookNavigationLink(title: "AlertController") { + BookPreview { _ in + let view = UIView(frame: .init(x: 0, y: 0, width: 80, height: 80)) + view.backgroundColor = .systemPurple + return view + } + .previewFrame(maxWidth: .greatestFiniteMagnitude, idealHeight: 10) - BookPresent(title: "Pop") { - let alert = UIAlertController( - title: "Hi Storybook", - message: "As like this, you can present any view controller to check the behavior.", - preferredStyle: .alert - ) - alert.addAction(.init(title: "Got it", style: .default, handler: { _ in })) - return alert - } + } - BookPresent(title: "Another Pop") { - let alert = UIAlertController( - title: "Hi Storybook", - message: "As like this, you can present any view controller to check the behavior.", - preferredStyle: .alert - ) - alert.addAction(.init(title: "Got it", style: .default, handler: { _ in })) - return alert } + BookNavigationLink(title: "AlertController") { + + BookPresent(title: "Pop") { + let alert = UIAlertController( + title: "Hi Storybook", + message: "As like this, you can present any view controller to check the behavior.", + preferredStyle: .alert + ) + alert.addAction(.init(title: "Got it", style: .default, handler: { _ in })) + return alert + } - } + BookPresent(title: "Another Pop") { + let alert = UIAlertController( + title: "Hi Storybook", + message: "As like this, you can present any view controller to check the behavior.", + preferredStyle: .alert + ) + alert.addAction(.init(title: "Got it", style: .default, handler: { _ in })) + return alert + } - BookNavigationLink(title: "Test Push") { - BookPush(title: "Test") { - UIViewController() } - } - BookNavigationLink(title: "State") { - BookPreview(title: "State: Success") { _ in - MySuccessView() + BookNavigationLink(title: "Test Push") { + BookPush(title: "Test") { + UIViewController() + } } - BookPreview(title: "State: Loading") { _ in - MyLoadingView() - } + BookNavigationLink(title: "State") { + BookPreview(title: "State: Success") { _ in + MySuccessView() + } + + BookPreview(title: "State: Loading") { _ in + MyLoadingView() + } - BookPreview(title: "State: Error") { _ in - MyErrorView() + BookPreview(title: "State: Error") { _ in + MyErrorView() + } } - } - BookNavigationLink(title: "Pattern") { + BookNavigationLink(title: "Pattern") { - BookPattern.make( - ["A", "AAA", "AAAAAA"], - [UIColor.blue, UIColor.red, UIColor.orange] - ) - .makeBody { args in - BookPreview { _ in - let (text, color) = args - let label = UILabel() - label.text = text - label.textColor = color - return label + BookPattern.make( + ["A", "AAA", "AAAAAA"], + [UIColor.blue, UIColor.red, UIColor.orange] + ) + .makeBody { args in + BookPreview { _ in + let (text, color) = args + let label = UILabel() + label.text = text + label.textColor = color + return label + } } - } + } } + BookFolder(title: "B") { + } } - ) + + } ) private func labelExpandingTestBook() -> some View { diff --git a/Development/Demo/RootView.swift b/Development/Demo/RootView.swift index 613d092..4588c0f 100644 --- a/Development/Demo/RootView.swift +++ b/Development/Demo/RootView.swift @@ -33,7 +33,7 @@ struct RootView: View { // book: myBook // ) - StorybookDisplayRootView2(book: myBook2) + StorybookDisplayRootView(bookStore: .init(book: myBook2)) } } diff --git a/Package.resolved b/Package.resolved deleted file mode 100644 index ae6c658..0000000 --- a/Package.resolved +++ /dev/null @@ -1,68 +0,0 @@ -{ - "pins" : [ - { - "identity" : "descriptors", - "kind" : "remoteSourceControl", - "location" : "https://github.com/FluidGroup/Descriptors", - "state" : { - "revision" : "f41ce2605a76c5d378fe8c5e8c5c98b544dfd108", - "version" : "0.2.3" - } - }, - { - "identity" : "mondrianlayout", - "kind" : "remoteSourceControl", - "location" : "https://github.com/muukii/MondrianLayout.git", - "state" : { - "revision" : "5f00b13984fe08316fc5b5be06e2f41c14a3befa", - "version" : "0.10.0" - } - }, - { - "identity" : "swiftui-gesture-velocity", - "kind" : "remoteSourceControl", - "location" : "https://github.com/FluidGroup/swiftui-gesture-velocity", - "state" : { - "revision" : "9c83f8995f9e5efc29db2fca4b9ff058283f1603", - "version" : "1.0.0" - } - }, - { - "identity" : "swiftui-support", - "kind" : "remoteSourceControl", - "location" : "https://github.com/FluidGroup/swiftui-support", - "state" : { - "revision" : "8ef53190c33bd345e7a95ef504dafe0f85ad9c4d", - "version" : "0.4.1" - } - }, - { - "identity" : "texture", - "kind" : "remoteSourceControl", - "location" : "https://github.com/FluidGroup/Texture.git", - "state" : { - "branch" : "spm", - "revision" : "17e0b467fe7360cfb29f2612785acf60e86775e9" - } - }, - { - "identity" : "texturebridging", - "kind" : "remoteSourceControl", - "location" : "https://github.com/FluidGroup/TextureBridging.git", - "state" : { - "branch" : "main", - "revision" : "ed8d3ac84c3fda90832c793fdeb8972154bd15fe" - } - }, - { - "identity" : "textureswiftsupport", - "kind" : "remoteSourceControl", - "location" : "https://github.com/FluidGroup/TextureSwiftSupport.git", - "state" : { - "branch" : "main", - "revision" : "5ebbc98ddfdac136620a95b7cf01894f4863f811" - } - } - ], - "version" : 2 -} diff --git a/Sources/StorybookKit/Primitives/Book.swift b/Sources/StorybookKit/Primitives/Book.swift index e9df651..b8b92f7 100644 --- a/Sources/StorybookKit/Primitives/Book.swift +++ b/Sources/StorybookKit/Primitives/Book.swift @@ -30,112 +30,22 @@ public protocol BookType: View { } -public final class BookStore: ObservableObject { - - @Published var historyPages: [BookNavigationLink] = [] +public struct Book { public let title: String - public let groups: [BookPageGroup] - - private let allPages: [BookNavigationLink] - - private let userDefaults = UserDefaults(suiteName: "jp.eure.storybook2") ?? .standard + public let folders: [BookFolder] public init( title: String, - @ArrayBuilder groups: () -> [BookPageGroup] + @ArrayBuilder folders: () -> [BookFolder] ) { self.title = title - self.groups = groups().sorted(by: { $0.title < $1.title }) - self.allPages = self.groups.flatMap { $0.pages } - - updateHistory() - } - - private func updateHistory() { - - let indexes = userDefaults.array(forKey: "history") as? [Int] ?? [] - - let _links = indexes.compactMap { index -> BookNavigationLink? in - guard let page = allPages.first(where: { $0.declarationIdentifier.index == index }) else { - return nil - } - return page - } - - historyPages = _links - - } - - func onOpen(link: BookNavigationLink) { - - guard allPages.contains(where: { $0.id == link.id }) else { - return - } - - let index = link.declarationIdentifier.index - - var current = userDefaults.array(forKey: "history") as? [Int] ?? [] - if let index = current.firstIndex(of: index) { - current.remove(at: index) - } - - current.insert(index, at: 0) - current = Array(current.prefix(5)) - - userDefaults.set(current, forKey: "history") - - print("Update history", current) - - updateHistory() - } - -} - -public struct BookContainer: BookType { - - @ObservedObject var store: BookStore - - @MainActor - public init( - store: BookStore - ) { - self.store = store - } - - public var body: some View { - NavigationView { - List { - Section { - ForEach(store.historyPages) { link in - link - } - } header: { - Text("History") - } - - ForEach(store.groups) { group in - Section { - group - } header: { - Text(group.title) - } - } - } - .listStyle(.grouped) - } - .navigationTitle(store.title) - .environment(\.bookContext, store) + self.folders = folders() } } -public final class BookContext { - func onOpen(link: BookNavigationLink) { - print(link) - } -} private enum BookContextKey: EnvironmentKey { static var defaultValue: BookStore? diff --git a/Sources/StorybookKit/Primitives/BookFolder.swift b/Sources/StorybookKit/Primitives/BookFolder.swift new file mode 100644 index 0000000..fb157c9 --- /dev/null +++ b/Sources/StorybookKit/Primitives/BookFolder.swift @@ -0,0 +1,117 @@ +import SwiftUI + +public struct BookFolder: BookView, Identifiable { + + public enum Node: Identifiable { + + public var id: String { + switch self { + case .folder(let v): return "folder.\(v.id.uuidString)" + case .group(let v): return "group.\(v.id.uuidString)" + } + } + + case folder(BookFolder) + case group(BookPageGroup) + } + + public var id: UUID = .init() + + public let title: String + public let contents: [Node] + + public init( + title: String, + @FolderBuilder contents: () -> [Node] + ) { + + self.title = title + self.contents = contents() + + } + + public var body: some View { + ForEach(contents) { node in + switch node { + case .folder(let folder): + NavigationLink { + folder + } label: { + HStack { + Image.init(systemName: "folder") + Text(folder.title) + } + } + case .group(let group): + group + } + } + } + + func allPages() -> [BookPage] { + contents.flatMap { node -> [BookPage] in + switch node { + case .folder(let folder): + return folder.allPages() + case .group(let group): + return group.pages + } + } + } + +} + +@resultBuilder +public struct FolderBuilder { + + public typealias Element = BookFolder.Node + + public static func buildExpression(_ expression: BookPageGroup) -> [FolderBuilder.Element] { + return [.group(expression)] + } + + public static func buildExpression(_ expression: BookFolder) -> [FolderBuilder.Element] { + return [.folder(expression)] + } + + public static func buildBlock() -> [Element] { + [] + } + + public static func buildBlock(_ contents: C...) -> [Element] where C.Element == Element { + return contents.flatMap { $0 } + } + + public static func buildOptional(_ component: [Element]?) -> [Element] { + return component ?? [] + } + + public static func buildEither(first component: [Element]) -> [Element] { + return component + } + + public static func buildEither(second component: [Element]) -> [Element] { + return component + } + + public static func buildArray(_ components: [[Element]]) -> [Element] { + components.flatMap { $0 } + } + + public static func buildExpression(_ element: Element?) -> [Element] { + return element.map { [$0] } ?? [] + } + + public static func buildExpression(_ element: Element) -> [Element] { + return [element] + } + + public static func buildExpression(_ elements: C) -> [Element] where C.Element == Element { + Array(elements) + } + + public static func buildExpression(_ elements: C) -> [Element] where C.Element == Optional { + elements.compactMap { $0 } + } + +} diff --git a/Sources/StorybookKit/Primitives/BookNavigationLink.swift b/Sources/StorybookKit/Primitives/BookNavigationLink.swift index 2004d08..f77c20d 100644 --- a/Sources/StorybookKit/Primitives/BookNavigationLink.swift +++ b/Sources/StorybookKit/Primitives/BookNavigationLink.swift @@ -30,6 +30,10 @@ public struct DeclarationIdentifier: Hashable, Codable { nonisolated init() { index = issueUniqueNumber() } + + public init(raw index: Int) { + self.index = index + } } private let _lock = NSLock() @@ -90,18 +94,20 @@ public struct BookNavigationLink: BookView, Identifiable { public var body: some View { - NavigationLink( - title, - destination: { - List { - destination - } - .listStyle(.plain) - .onAppear(perform: { - context?.onOpen(link: self) - }) + NavigationLink { + List { + destination } - ) + .listStyle(.plain) + .onAppear(perform: { + context?.onOpen(page: self) + }) + } label: { + HStack { + Image.init(systemName: "doc") + Text(title) + } + } } } diff --git a/Sources/StorybookKit/Primitives/BookStore.swift b/Sources/StorybookKit/Primitives/BookStore.swift new file mode 100644 index 0000000..5cd435a --- /dev/null +++ b/Sources/StorybookKit/Primitives/BookStore.swift @@ -0,0 +1,78 @@ + + +public final class BookStore: ObservableObject { + + @Published var historyPages: [BookNavigationLink] = [] + + public let title: String + public let folders: [BookFolder] + + private let allPages: [BookNavigationLink.ID: BookNavigationLink] + + private let userDefaults = UserDefaults(suiteName: "jp.eure.storybook2") ?? .standard + + public init( + book: Book + ) { + + self.title = book.title + + self.folders = book.folders + + self.allPages = book.folders.flatMap { + $0.contents.flatMap { + switch $0 { + case .folder(let v): return v.allPages() + case .group(let v): return v.pages + } + } + } + .reduce( + into: [BookNavigationLink.ID: BookNavigationLink](), + { partialResult, item in + partialResult[item.id] = item + } + ) + + updateHistory() + } + + private func updateHistory() { + + let indexes = userDefaults.array(forKey: "history") as? [Int] ?? [] + + let _pages = indexes.compactMap { index -> BookPage? in + guard let page = allPages[.init(raw: index)] else { + return nil + } + return page + } + + historyPages = _pages + + } + + func onOpen(page: BookPage) { + + guard allPages.keys.contains(page.id) else { + return + } + + let index = page.declarationIdentifier.index + + var current = userDefaults.array(forKey: "history") as? [Int] ?? [] + if let index = current.firstIndex(of: index) { + current.remove(at: index) + } + + current.insert(index, at: 0) + current = Array(current.prefix(5)) + + userDefaults.set(current, forKey: "history") + + print("Update history", current) + + updateHistory() + } + +} diff --git a/Sources/StorybookKit/StorybookDisplayRootView.swift b/Sources/StorybookKit/StorybookDisplayRootView.swift index 821307f..81ad6b8 100644 --- a/Sources/StorybookKit/StorybookDisplayRootView.swift +++ b/Sources/StorybookKit/StorybookDisplayRootView.swift @@ -2,12 +2,13 @@ import SwiftUI import SwiftUISupport import UIKit -public struct StorybookDisplayRootView: View { +public struct StorybookDisplayRootView: View { - public let book: Book + let book: BookContainer - public init(book: Book) { - self.book = book + @MainActor + public init(bookStore: BookStore) { + self.book = .init(store: bookStore) } public var body: some View { @@ -20,24 +21,43 @@ public struct StorybookDisplayRootView: View { } } -public struct StorybookDisplayRootView2: View { +struct BookContainer: BookType { - public let book: BookContainer + @ObservedObject var store: BookStore - public init(book: BookContainer) { - self.book = book + @MainActor + public init( + store: BookStore + ) { + self.store = store } public var body: some View { - - _ViewControllerHost { - let controller = _ViewController(book: book) - return controller + NavigationView { + List { + Section { + ForEach(store.historyPages) { link in + link + } + } header: { + Text("History") + } + + ForEach(store.folders) { folder in + Section { + folder + } header: { + Text("Contents") + } + } + } + .listStyle(.grouped) } - + .navigationTitle(store.title) + .environment(\.bookContext, store) } -} +} final class _ViewController: UIViewController {