From c4d8b9d82962b72fc2aa90db272359ae5ca4933b Mon Sep 17 00:00:00 2001
From: ra1028
Date: Tue, 13 Feb 2024 16:22:19 +0900
Subject: [PATCH 01/10] Implement new PlaybookUI
---
.../PlaybookExample.xcodeproj/project.pbxproj | 4 +-
.../Profiles/HikeView.swift | 1 -
.../Supporting Views/GraphCapsule.swift | 4 +-
.../Scenarios/AllScenarios.swift | 1 +
Example/project.yml | 2 +-
Playbook.xcodeproj/project.pbxproj | 296 +++++++------
.../Internal/SnapshotWindow.swift | 8 +-
Sources/PlaybookUI/Internal/Atomic.swift | 32 --
Sources/PlaybookUI/Internal/Blur.swift | 13 -
.../PlaybookUI/Internal/CatalogBarItem.swift | 17 -
.../Internal/CatalogDrawerStyle.swift | 41 --
.../Internal/CatalogSplitStyle.swift | 59 ---
.../PlaybookUI/Internal/CatalogStore.swift | 37 --
Sources/PlaybookUI/Internal/Counter.swift | 19 -
Sources/PlaybookUI/Internal/Drawer.swift | 112 -----
.../Internal/Entities/SearchResult.swift | 5 +
.../Internal/Entities/SearchedData.swift | 7 +
.../Internal/Entities/SearchedKindData.swift | 7 +
.../Internal/Entities/SelectData.swift | 15 +
.../Internal/EnvironmentValues.swift | 31 --
Sources/PlaybookUI/Internal/Extensions.swift | 86 ----
.../PlaybookUI/Internal/GalleryStore.swift | 96 -----
Sources/PlaybookUI/Internal/Highlight.swift | 17 -
.../Internal/HorizontalSeparator.swift | 10 -
.../Internal/ImageSharingView.swift | 35 --
.../Internal/KeyboardDismissProxy.swift | 43 --
.../Internal/ScenarioContentView.swift | 47 --
.../PlaybookUI/Internal/ScenarioDisplay.swift | 130 ------
.../Internal/ScenarioDisplayList.swift | 85 ----
.../Internal/ScenarioDisplaySheet.swift | 115 -----
.../Internal/ScenarioDisplayStore.swift | 108 -----
.../Internal/ScenarioSearchStore.swift | 90 ----
.../Internal/ScenarioSearchTree.swift | 406 ------------------
Sources/PlaybookUI/Internal/Scheduler.swift | 16 -
Sources/PlaybookUI/Internal/SearchBar.swift | 78 ----
.../PlaybookUI/Internal/SearchResult.swift | 4 -
.../PlaybookUI/Internal/SearchedData.swift | 14 -
.../Internal/SearchedListData.swift | 5 -
.../Internal/SerialMainDispatcher.swift | 80 ----
.../PlaybookUI/Internal/SnapshotLoader.swift | 86 ----
.../Internal/State/CatalogState.swift | 49 +++
.../Internal/State/GalleryState.swift | 14 +
.../Internal/State/SearchState.swift | 88 ++++
.../Internal/State/ShareState.swift | 37 ++
Sources/PlaybookUI/Internal/TableView.swift | 137 ------
.../PlaybookUI/Internal/Utilities/Image.swift | 21 +
.../Internal/Utilities/ImageCache.swift | 89 ++++
.../Internal/Utilities/ImageLoader.swift | 135 ++++++
.../Internal/Utilities/ImageSource.swift | 10 +
.../Internal/Utilities/Spacer.swift | 11 +
.../Internal/Utilities/UIColor.swift | 21 +
.../Internal/Utilities/UIImage.swift | 123 ++++++
.../PlaybookUI/Internal/Utilities/View.swift | 26 ++
.../Internal/Views/CatalogBottomBar.swift | 63 +++
.../Internal/Views/CatalogDrawer.swift | 59 +++
.../Internal/Views/CatalogKindRow.swift | 42 ++
.../Internal/Views/CatalogScenarioRow.swift | 37 ++
.../Internal/Views/CatalogSearchPane.swift | 92 ++++
.../Internal/Views/CatalogSplit.swift | 35 ++
.../Internal/Views/CatalogTop.swift | 38 ++
.../Internal/Views/ColorSchemePicker.swift | 32 ++
.../PlaybookUI/Internal/Views/Counter.swift | 20 +
.../Internal/Views/GalleryDetail.swift | 34 ++
.../Internal/Views/GalleryDetailTopBar.swift | 60 +++
.../Internal/Views/GalleryKindRow.swift | 50 +++
.../Internal/Views/GalleryThumbnail.swift | 114 +++++
.../Internal/Views/HighlightText.swift | 30 ++
.../Internal/Views/MaterialView.swift | 10 +
.../Internal/Views/ScenarioContentView.swift | 24 ++
.../PlaybookUI/Internal/Views/SearchBar.swift | 78 ++++
.../PlaybookUI/Internal/Views/Separator.swift | 13 +
.../Internal/Views/UnavailableView.swift | 24 ++
.../PlaybookUI/Internal/WeakReference.swift | 13 -
Sources/PlaybookUI/PlaybookCatalog.swift | 216 ++--------
Sources/PlaybookUI/PlaybookGallery.swift | 400 ++++-------------
Tests/SnapshotTests.swift | 1 -
76 files changed, 1822 insertions(+), 2686 deletions(-)
delete mode 100644 Sources/PlaybookUI/Internal/Atomic.swift
delete mode 100644 Sources/PlaybookUI/Internal/Blur.swift
delete mode 100644 Sources/PlaybookUI/Internal/CatalogBarItem.swift
delete mode 100644 Sources/PlaybookUI/Internal/CatalogDrawerStyle.swift
delete mode 100644 Sources/PlaybookUI/Internal/CatalogSplitStyle.swift
delete mode 100644 Sources/PlaybookUI/Internal/CatalogStore.swift
delete mode 100644 Sources/PlaybookUI/Internal/Counter.swift
delete mode 100644 Sources/PlaybookUI/Internal/Drawer.swift
create mode 100644 Sources/PlaybookUI/Internal/Entities/SearchResult.swift
create mode 100644 Sources/PlaybookUI/Internal/Entities/SearchedData.swift
create mode 100644 Sources/PlaybookUI/Internal/Entities/SearchedKindData.swift
create mode 100644 Sources/PlaybookUI/Internal/Entities/SelectData.swift
delete mode 100644 Sources/PlaybookUI/Internal/EnvironmentValues.swift
delete mode 100644 Sources/PlaybookUI/Internal/Extensions.swift
delete mode 100644 Sources/PlaybookUI/Internal/GalleryStore.swift
delete mode 100644 Sources/PlaybookUI/Internal/Highlight.swift
delete mode 100644 Sources/PlaybookUI/Internal/HorizontalSeparator.swift
delete mode 100644 Sources/PlaybookUI/Internal/ImageSharingView.swift
delete mode 100644 Sources/PlaybookUI/Internal/KeyboardDismissProxy.swift
delete mode 100644 Sources/PlaybookUI/Internal/ScenarioContentView.swift
delete mode 100644 Sources/PlaybookUI/Internal/ScenarioDisplay.swift
delete mode 100644 Sources/PlaybookUI/Internal/ScenarioDisplayList.swift
delete mode 100644 Sources/PlaybookUI/Internal/ScenarioDisplaySheet.swift
delete mode 100644 Sources/PlaybookUI/Internal/ScenarioDisplayStore.swift
delete mode 100644 Sources/PlaybookUI/Internal/ScenarioSearchStore.swift
delete mode 100644 Sources/PlaybookUI/Internal/ScenarioSearchTree.swift
delete mode 100644 Sources/PlaybookUI/Internal/Scheduler.swift
delete mode 100644 Sources/PlaybookUI/Internal/SearchBar.swift
delete mode 100644 Sources/PlaybookUI/Internal/SearchResult.swift
delete mode 100644 Sources/PlaybookUI/Internal/SearchedData.swift
delete mode 100644 Sources/PlaybookUI/Internal/SearchedListData.swift
delete mode 100644 Sources/PlaybookUI/Internal/SerialMainDispatcher.swift
delete mode 100644 Sources/PlaybookUI/Internal/SnapshotLoader.swift
create mode 100644 Sources/PlaybookUI/Internal/State/CatalogState.swift
create mode 100644 Sources/PlaybookUI/Internal/State/GalleryState.swift
create mode 100644 Sources/PlaybookUI/Internal/State/SearchState.swift
create mode 100644 Sources/PlaybookUI/Internal/State/ShareState.swift
delete mode 100644 Sources/PlaybookUI/Internal/TableView.swift
create mode 100644 Sources/PlaybookUI/Internal/Utilities/Image.swift
create mode 100644 Sources/PlaybookUI/Internal/Utilities/ImageCache.swift
create mode 100644 Sources/PlaybookUI/Internal/Utilities/ImageLoader.swift
create mode 100644 Sources/PlaybookUI/Internal/Utilities/ImageSource.swift
create mode 100644 Sources/PlaybookUI/Internal/Utilities/Spacer.swift
create mode 100644 Sources/PlaybookUI/Internal/Utilities/UIColor.swift
create mode 100644 Sources/PlaybookUI/Internal/Utilities/UIImage.swift
create mode 100644 Sources/PlaybookUI/Internal/Utilities/View.swift
create mode 100644 Sources/PlaybookUI/Internal/Views/CatalogBottomBar.swift
create mode 100644 Sources/PlaybookUI/Internal/Views/CatalogDrawer.swift
create mode 100644 Sources/PlaybookUI/Internal/Views/CatalogKindRow.swift
create mode 100644 Sources/PlaybookUI/Internal/Views/CatalogScenarioRow.swift
create mode 100644 Sources/PlaybookUI/Internal/Views/CatalogSearchPane.swift
create mode 100644 Sources/PlaybookUI/Internal/Views/CatalogSplit.swift
create mode 100644 Sources/PlaybookUI/Internal/Views/CatalogTop.swift
create mode 100644 Sources/PlaybookUI/Internal/Views/ColorSchemePicker.swift
create mode 100644 Sources/PlaybookUI/Internal/Views/Counter.swift
create mode 100644 Sources/PlaybookUI/Internal/Views/GalleryDetail.swift
create mode 100644 Sources/PlaybookUI/Internal/Views/GalleryDetailTopBar.swift
create mode 100644 Sources/PlaybookUI/Internal/Views/GalleryKindRow.swift
create mode 100644 Sources/PlaybookUI/Internal/Views/GalleryThumbnail.swift
create mode 100644 Sources/PlaybookUI/Internal/Views/HighlightText.swift
create mode 100644 Sources/PlaybookUI/Internal/Views/MaterialView.swift
create mode 100644 Sources/PlaybookUI/Internal/Views/ScenarioContentView.swift
create mode 100644 Sources/PlaybookUI/Internal/Views/SearchBar.swift
create mode 100644 Sources/PlaybookUI/Internal/Views/Separator.swift
create mode 100644 Sources/PlaybookUI/Internal/Views/UnavailableView.swift
delete mode 100644 Sources/PlaybookUI/Internal/WeakReference.swift
diff --git a/Example/PlaybookExample.xcodeproj/project.pbxproj b/Example/PlaybookExample.xcodeproj/project.pbxproj
index 082f568..31242bb 100644
--- a/Example/PlaybookExample.xcodeproj/project.pbxproj
+++ b/Example/PlaybookExample.xcodeproj/project.pbxproj
@@ -983,7 +983,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
@@ -1043,7 +1043,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
diff --git a/Example/SampleComponent/Apple - Working with UI Controls/Profiles/HikeView.swift b/Example/SampleComponent/Apple - Working with UI Controls/Profiles/HikeView.swift
index c76b771..1470025 100644
--- a/Example/SampleComponent/Apple - Working with UI Controls/Profiles/HikeView.swift
+++ b/Example/SampleComponent/Apple - Working with UI Controls/Profiles/HikeView.swift
@@ -24,7 +24,6 @@ public struct HikeView: View {
HStack {
HikeGraph(hike: hike, path: \.elevation)
.frame(width: 50, height: 30)
- .animation(nil)
VStack(alignment: .leading) {
Text(verbatim: hike.name)
diff --git a/Example/SampleComponent/Apple - Working with UI Controls/Supporting Views/GraphCapsule.swift b/Example/SampleComponent/Apple - Working with UI Controls/Supporting Views/GraphCapsule.swift
index 3dfba15..e63928f 100644
--- a/Example/SampleComponent/Apple - Working with UI Controls/Supporting Views/GraphCapsule.swift
+++ b/Example/SampleComponent/Apple - Working with UI Controls/Supporting Views/GraphCapsule.swift
@@ -7,7 +7,7 @@ A single line in the graph.
import SwiftUI
-public struct GraphCapsule: View {
+public struct GraphCapsule: View, Equatable {
public var index: Int
public var height: CGFloat
public var range: Range
@@ -33,7 +33,7 @@ public struct GraphCapsule: View {
.fill(color)
.frame(height: height * heightRatio, alignment: .bottom)
.offset(x: 0, y: height * -offsetRatio)
- .animation(animation)
+ .animation(animation, value: self)
}
public init(
diff --git a/Example/SamplePlaybook/Scenarios/AllScenarios.swift b/Example/SamplePlaybook/Scenarios/AllScenarios.swift
index 982ff66..8ccce16 100644
--- a/Example/SamplePlaybook/Scenarios/AllScenarios.swift
+++ b/Example/SamplePlaybook/Scenarios/AllScenarios.swift
@@ -1,5 +1,6 @@
import Playbook
import SampleComponent
+import SwiftUI
struct AllScenarios: ScenarioProvider {
static func addScenarios(into playbook: Playbook) {
diff --git a/Example/project.yml b/Example/project.yml
index f6cdf42..e3bf412 100644
--- a/Example/project.yml
+++ b/Example/project.yml
@@ -7,7 +7,7 @@ options:
createIntermediateGroups: true
bundleIdPrefix: app.playbook-ui.Example
deploymentTarget:
- iOS: 13.0
+ iOS: 15.0
settings:
CODE_SIGNING_REQUIRED: NO
diff --git a/Playbook.xcodeproj/project.pbxproj b/Playbook.xcodeproj/project.pbxproj
index d50ea98..9fb93ad 100644
--- a/Playbook.xcodeproj/project.pbxproj
+++ b/Playbook.xcodeproj/project.pbxproj
@@ -9,77 +9,81 @@
/* Begin PBXBuildFile section */
04DAC4EBEE89D365F72AD5DB /* SnapshotSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 023425CAEE4AA111BE174F66 /* SnapshotSupport.swift */; };
04E6DA15062197311CB80A23 /* ScenarioProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68E1EB13B5ED8DD48245B1E8 /* ScenarioProvider.swift */; };
- 0D0659EBE8EF0971AF40405D /* Scheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2C24C0BB3864D3AF550A05D /* Scheduler.swift */; };
- 0E1A77DB134CE41112BE7EFB /* Counter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95BDCEF4FDC1DE7D50B6E73D /* Counter.swift */; };
- 0FEB2071F01AF72A62527F63 /* Highlight.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA6FEAB12579B7940EA763BB /* Highlight.swift */; };
- 1045A54325FE670A3EC64866 /* SearchedListData.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF2924992CEC971C20B6EC33 /* SearchedListData.swift */; };
+ 08DB4240FAF948F16398C1C9 /* SearchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = C509DD2858530167BEC5BB11 /* SearchResult.swift */; };
+ 0A26157EE6E7B940067CA211 /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 559B640B10AF07E0E4755344 /* SearchBar.swift */; };
+ 14CB92EA193DE1EEB60DC590 /* ColorSchemePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F44D4BCEA15399D487993CA /* ColorSchemePicker.swift */; };
14DBC83A78A328C77BF96AAF /* Playbook.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 08B7BB8752007FC3BB78EC8A /* Playbook.framework */; };
- 1561DDA840821DF8807FF006 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 636464CE7774B71958BDBCD2 /* Extensions.swift */; };
1742749E8A4E3AB081B5DCB2 /* PlaybookUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 0B038E054D3A0F626DD32C93 /* PlaybookUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
- 2336557C8E97894E4737A528 /* ScenarioDisplayStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621233CF50A0017066623146 /* ScenarioDisplayStore.swift */; };
- 25C326C9B1CBC90FC989C264 /* CatalogStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D6EC7EA43758CD4E10A70A8 /* CatalogStore.swift */; };
- 2835B28061CBB1F908CE8D14 /* SerialMainDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70BEAC578E82D5EE82664DA2 /* SerialMainDispatcher.swift */; };
+ 17740091AF2876BEA30F0E68 /* HighlightText.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCBE2887B8D2BD7EA50CD94B /* HighlightText.swift */; };
+ 1F6EB9E17EC82DFFE52C8FE9 /* MaterialView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85A9B46FFAE9273D84C8A395 /* MaterialView.swift */; };
2C4D9ABC6FC9696E42BE8080 /* PlaybookGallery.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6B0431D91C939D181493ECC /* PlaybookGallery.swift */; };
334B859A3103A83A4EC996BF /* GalleryScenarios.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BAD3FE8D577886981218938 /* GalleryScenarios.swift */; };
341406194DDF8C7A82AB96D0 /* ScenarioContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63CD111722A3B8067BFE548 /* ScenarioContext.swift */; };
358D72B04367A43CD12978FD /* ScenarioViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77D5B1C44FC9E87FD3237459 /* ScenarioViewController.swift */; };
- 3DFE3E1AF8E3691457DF156C /* SnapshotLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B7C02BA0BA547162205F277 /* SnapshotLoader.swift */; };
+ 3D179776ADD757235527A929 /* GalleryThumbnail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80C668330D834FEE619E6735 /* GalleryThumbnail.swift */; };
+ 4104FE671C498D67FB0134C3 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B12B3F422F9EE7600CA7B8C /* ImageCache.swift */; };
43EC47CA52837935B25B6A54 /* Playbook.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 08B7BB8752007FC3BB78EC8A /* Playbook.framework */; };
- 4F942C3581B6150EC113B378 /* ScenarioDisplaySheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8FCF24068A7FDD9BAD27ECA /* ScenarioDisplaySheet.swift */; };
+ 4C55A5EE142078443D368124 /* CatalogScenarioRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A940D28E31EFAE5B3694CEE /* CatalogScenarioRow.swift */; };
+ 4D66B6CDF20B4B1C3DEF6790 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ED586C95333C4B752C85423 /* UIImage.swift */; };
4FD59723F2D559B839F6AE9D /* ScenarioSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5527163A80E0C62E2C9C19F /* ScenarioSwiftUI.swift */; };
509D6D27E6A76DCBDEE64226 /* Export.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51782585DE3DFFD2FDB8684B /* Export.swift */; };
- 59BC00DE37B8F0ED2324F9E6 /* ScenarioDisplayList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF07EECC1D23D59334517855 /* ScenarioDisplayList.swift */; };
+ 5494FCBB0627B107CAF044DB /* SelectData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2532CFEE0AB6105AEB5B74E2 /* SelectData.swift */; };
+ 56FFC73C5EA4C3F7AB583136 /* SearchState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D26212C25465F186B196A9E9 /* SearchState.swift */; };
5B06509DE08626C2143A51C7 /* PlaybookUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0B038E054D3A0F626DD32C93 /* PlaybookUI.framework */; };
5CEC9BCEB07DA6F388499754 /* SnapshotWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B09750B60723BB88EB4E0BE /* SnapshotWindow.swift */; };
- 5D84ECFA16954ACC9A0E1141 /* TableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABBE0D7EADE227862CB3EB1F /* TableView.swift */; };
5DC9DF46A93B504BEC043D62 /* Snapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66200EEB348D4A81C09138E5 /* Snapshot.swift */; };
- 663722E1FA2A87F28409489D /* SearchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC9723566343A9120E2BD3D /* SearchResult.swift */; };
668D80F30C0A09A5667C592A /* PlaybookCatalog.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA762F5B447EF9828E1F86F2 /* PlaybookCatalog.swift */; };
- 6BC169CC10D3F962E48C7A60 /* ScenarioContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EC4A04F778E41A014D8DD46 /* ScenarioContentView.swift */; };
6CF7056823EA1730746AB145 /* ScenarioViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F913250C8A5AB7148459FEC6 /* ScenarioViewControllerTests.swift */; };
6FC965BE651532324127C71C /* ScenariosBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97823B5F50AA1F1689DB8F56 /* ScenariosBuilder.swift */; };
+ 71721E7FA3A9FC3017EEA4FC /* Spacer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE9E33C79F43CEDD6BA712 /* Spacer.swift */; };
72DDF1BB8E77EF11CB9BB4FC /* ScenarioSwiftUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB51C85B7D63CEF5DC83B60 /* ScenarioSwiftUITests.swift */; };
+ 748569229F0F50D9C36EE2E9 /* UnavailableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B5E679D86934A2FB5A0856 /* UnavailableView.swift */; };
78E68D7FE09DC81B3F3659F7 /* Playbook.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 08B7BB8752007FC3BB78EC8A /* Playbook.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
- 795BBCD562B512FC4ED96448 /* CatalogBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB7DE56F774FFD4DB73EBD37 /* CatalogBarItem.swift */; };
- 80BBDCCF2C1580F0F508ED73 /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1721044B445E0A9326CA6D21 /* Atomic.swift */; };
- 819E05B5E7ABEB948250AD8C /* SearchedData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 369C805F890E0F0A27A44B5F /* SearchedData.swift */; };
- 88C7D7A743A333E7DF642F54 /* ScenarioSearchStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49057856849F915AB692364 /* ScenarioSearchStore.swift */; };
+ 7AF75286918869ED0516B47D /* CatalogTop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4846AD7448E08DBA3E2C18A5 /* CatalogTop.swift */; };
+ 8298234AB8AC2B9046BE09D0 /* GalleryKindRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECE9B3C751E8D95A68E247DF /* GalleryKindRow.swift */; };
+ 8C596670DA8078B4CA2AA743 /* CatalogSplit.swift in Sources */ = {isa = PBXBuildFile; fileRef = C925FCA9E427334E3128E21D /* CatalogSplit.swift */; };
+ 8DDAA3EF4EB3E70B9360432F /* Separator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11067B4E4A9D6494867D706A /* Separator.swift */; };
8E518A8B8563E1B97B7A61D7 /* PlaybookSnapshot.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 96A0C0FCA9FA7F91E01BB5A7 /* PlaybookSnapshot.framework */; };
8FB5292F077C2933737D8411 /* Export.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89B9EFFA662D997B8B95EE74 /* Export.swift */; };
9399EAA20EE0DD10A8D65C9B /* SnapshotWaiter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0032B9D0F6976A6677402EB0 /* SnapshotWaiter.swift */; };
- 97F1D39B80BA6FC503E6C1DB /* KeyboardDismissProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95D5B5D2F570B29D57807A44 /* KeyboardDismissProxy.swift */; };
99904F2A1ECCD16DC130B51A /* OrderedStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25344536B72D76889805B50D /* OrderedStorage.swift */; };
+ 99A112D26891F0ED999FE27B /* ImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A05522F96443D07E69B0DD3 /* ImageLoader.swift */; };
9AD198DB3869534A7E503A66 /* Playbook.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 08B7BB8752007FC3BB78EC8A /* Playbook.framework */; };
- 9C10E142F8D14B64C893B9BD /* ScenarioDisplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39688046A26BC35A5FDFC672 /* ScenarioDisplay.swift */; };
- A28E3CC1C92EE68DEE3A627E /* ImageSharingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E955D716921B9CE34F4E35AC /* ImageSharingView.swift */; };
- A3236C9B14BBF8D34C16BC45 /* CatalogSplitStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E59E35FC704B6D9414A6DC /* CatalogSplitStyle.swift */; };
+ 9CBF0652A467E45FB7187988 /* UIColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF2169FF71AF568BF427357 /* UIColor.swift */; };
+ A1CD039E4B6D017210DEFF0E /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 391B584F5DA7B584216E4619 /* View.swift */; };
+ AD7EFE7D41A4801FEABBF156 /* CatalogDrawer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FC40B940F3E03A47FEC3EC8 /* CatalogDrawer.swift */; };
+ AE0DE7D74E721B0D254353B2 /* ScenarioContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B446C3A548A2DC847F032CA0 /* ScenarioContentView.swift */; };
AF9004569675F2D36F6108AE /* ScenarioStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6CEC4F17D84235922932498 /* ScenarioStore.swift */; };
AFD49E9D98CDDBEBAA1D9E3A /* ScenariosBuildable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36894DCD112EFAAEE670D92 /* ScenariosBuildable.swift */; };
+ B1B7C962BA824C7739DD7486 /* ShareState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B2681920272C74A1032D3A4 /* ShareState.swift */; };
+ B257A455408A87DD7A0D9F6B /* Counter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 556DC8B6487B074BB222CDA9 /* Counter.swift */; };
B366B5D2A9ECB2E5289FDCD1 /* AllScenarios.swift in Sources */ = {isa = PBXBuildFile; fileRef = 302C484FF86103B73D1C1122 /* AllScenarios.swift */; };
- B4384D25712D57EBEDE149F6 /* CatalogDrawerStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377D53C09A49B19955FA86BE /* CatalogDrawerStyle.swift */; };
- B89DF49D830D61DA0B3C084D /* Drawer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C65E03F37BA759FC6E55D1C2 /* Drawer.swift */; };
+ B44C35245CFBC0043183D67C /* ImageSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA6720986B0B52A13B30D528 /* ImageSource.swift */; };
B945D03AD556F3F8B3161147 /* ScenarioStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81538A32C0A57BA96700E55E /* ScenarioStoreTests.swift */; };
BE7BF04B28E47309D2D7608D /* ScenarioKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = F830987472F2FBD2C3BA7284 /* ScenarioKind.swift */; };
BF44BEAFAA1183BE4B9952AF /* Scenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E50413A08A9CF285D97A65C /* Scenario.swift */; };
- C0F32DC787F41414E09C37FB /* HorizontalSeparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5576F4CB67593B259E12C387 /* HorizontalSeparator.swift */; };
C51E0AB09C31FA61FE7D90BF /* ExtraScenarios.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D23A73BBBCC7E6D4027CDB /* ExtraScenarios.swift */; };
- C6A0A71A7D1DB2BECD5B4A2D /* ScenarioSearchTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = 068FE06929659477F04AA176 /* ScenarioSearchTree.swift */; };
+ C57746152ACAC8D242E7992A /* GalleryState.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEC649C8437116C1FFCA36D9 /* GalleryState.swift */; };
CF8244A4AB05E5ED99B68742 /* SnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B59B5A8B33031C6AF8A049E /* SnapshotTests.swift */; };
D016FBABE74D6996F5648DDC /* SnapshotDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C5F051089936C546DFBB95 /* SnapshotDevice.swift */; };
- D45CD95A63FC59D24E4F5E65 /* GalleryStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F7F9F34307C4DEEDF210F9 /* GalleryStore.swift */; };
+ D97CC8D3208EEDF6B11EF898 /* SearchedKindData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73329E8A55711E5737C66ACD /* SearchedKindData.swift */; };
D99764328B0478E65C0FDEB0 /* ScenarioName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F065DE6EA72726CE6FE821D /* ScenarioName.swift */; };
DDF763A23C1AE9464827CCCE /* ScenarioLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF835DB024B80E2FADFFD0FC /* ScenarioLayout.swift */; };
- E1680C7D25ADB69BF62A6BF5 /* Blur.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2CD5659122DCA1911F566F /* Blur.swift */; };
+ DE001E8A450A047BE1F2AB7C /* CatalogKindRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B268BD03E4ADE4AE1F5BF269 /* CatalogKindRow.swift */; };
E5C77A25F77EC8ED48C13512 /* SnapshotError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 108C38EAC8F960E10B0FA627 /* SnapshotError.swift */; };
E7597D8F364FF8D2CD4DF584 /* PlaybookTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9370D44977281AAEBF6DEF44 /* PlaybookTests.swift */; };
+ EC542FAB97124C8728AE06DD /* CatalogState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8908E9DA16AB4BA3A659914C /* CatalogState.swift */; };
+ EC7FB83606201C8AAA15D098 /* GalleryDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 676CCC73E9C6DED60834DCF0 /* GalleryDetail.swift */; };
+ EE5F75875FDD118B4A5F0BF8 /* CatalogBottomBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698312DFD253858E0CE488E6 /* CatalogBottomBar.swift */; };
EEBAD73009B0B8CC6B4B6F76 /* TestTool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8700BC5E0B042913C8C10CBA /* TestTool.swift */; };
+ F16829BEF3BB1173FFFA18B0 /* SearchedData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F412670B8570C6C62C7875E /* SearchedData.swift */; };
F566A7702FD9DDCF20F450F2 /* CatalogScenarios.swift in Sources */ = {isa = PBXBuildFile; fileRef = D15384CB93B122425863A487 /* CatalogScenarios.swift */; };
- F68746E7D5745E764F19D377 /* EnvironmentValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4E86485B651D2EE3F6A2A6 /* EnvironmentValues.swift */; };
+ F7D7D9ADA3D68C3C17B9968D /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = B47949D75D9EE00D1B41E460 /* Image.swift */; };
F9DF0B63F62E3A7B5ED2E619 /* Mocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEB78746E7DE13CB4BBD42FC /* Mocks.swift */; };
- FAB1302DAAB4D0C5CF83C1C1 /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4351CB488B9D56ABCDD4E27A /* SearchBar.swift */; };
+ FB311A5E12E4B63A845D274A /* CatalogSearchPane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028BA6B86229E5CB18B406C1 /* CatalogSearchPane.swift */; };
FD3C77D1442C7CAADC579CB2 /* Playbook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C5E36DF37E9A2AF379859 /* Playbook.swift */; };
+ FE054384773081FAFD2F3A55 /* GalleryDetailTopBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2CB89FE909D51ACCC78465 /* GalleryDetailTopBar.swift */; };
FEE775B5BE1C7E957E0306DA /* PlaybookSnapshot.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 96A0C0FCA9FA7F91E01BB5A7 /* PlaybookSnapshot.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
- FF4469309B532A58B10C5EE1 /* WeakReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CED8582C39C01B51301E1CF /* WeakReference.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -139,72 +143,76 @@
/* Begin PBXFileReference section */
0032B9D0F6976A6677402EB0 /* SnapshotWaiter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnapshotWaiter.swift; sourceTree = ""; };
023425CAEE4AA111BE174F66 /* SnapshotSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnapshotSupport.swift; sourceTree = ""; };
- 068FE06929659477F04AA176 /* ScenarioSearchTree.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScenarioSearchTree.swift; sourceTree = ""; };
+ 028BA6B86229E5CB18B406C1 /* CatalogSearchPane.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogSearchPane.swift; sourceTree = ""; };
08B7BB8752007FC3BB78EC8A /* Playbook.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Playbook.framework; sourceTree = BUILT_PRODUCTS_DIR; };
0B038E054D3A0F626DD32C93 /* PlaybookUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PlaybookUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
108C38EAC8F960E10B0FA627 /* SnapshotError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnapshotError.swift; sourceTree = ""; };
- 1721044B445E0A9326CA6D21 /* Atomic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = ""; };
- 1CED8582C39C01B51301E1CF /* WeakReference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakReference.swift; sourceTree = ""; };
+ 11067B4E4A9D6494867D706A /* Separator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Separator.swift; sourceTree = ""; };
+ 2532CFEE0AB6105AEB5B74E2 /* SelectData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectData.swift; sourceTree = ""; };
25344536B72D76889805B50D /* OrderedStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderedStorage.swift; sourceTree = ""; };
+ 29B5E679D86934A2FB5A0856 /* UnavailableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnavailableView.swift; sourceTree = ""; };
+ 2B12B3F422F9EE7600CA7B8C /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = ""; };
2F065DE6EA72726CE6FE821D /* ScenarioName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScenarioName.swift; sourceTree = ""; };
+ 2F412670B8570C6C62C7875E /* SearchedData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchedData.swift; sourceTree = ""; };
+ 2F44D4BCEA15399D487993CA /* ColorSchemePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorSchemePicker.swift; sourceTree = ""; };
302C484FF86103B73D1C1122 /* AllScenarios.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllScenarios.swift; sourceTree = ""; };
- 369C805F890E0F0A27A44B5F /* SearchedData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchedData.swift; sourceTree = ""; };
- 377D53C09A49B19955FA86BE /* CatalogDrawerStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogDrawerStyle.swift; sourceTree = ""; };
385C5E36DF37E9A2AF379859 /* Playbook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Playbook.swift; sourceTree = ""; };
- 38F7F9F34307C4DEEDF210F9 /* GalleryStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryStore.swift; sourceTree = ""; };
- 39688046A26BC35A5FDFC672 /* ScenarioDisplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScenarioDisplay.swift; sourceTree = ""; };
- 4351CB488B9D56ABCDD4E27A /* SearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = ""; };
+ 391B584F5DA7B584216E4619 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = ""; };
+ 3FC40B940F3E03A47FEC3EC8 /* CatalogDrawer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogDrawer.swift; sourceTree = ""; };
+ 4846AD7448E08DBA3E2C18A5 /* CatalogTop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogTop.swift; sourceTree = ""; };
+ 4CEE9E33C79F43CEDD6BA712 /* Spacer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Spacer.swift; sourceTree = ""; };
51782585DE3DFFD2FDB8684B /* Export.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Export.swift; sourceTree = ""; };
- 5576F4CB67593B259E12C387 /* HorizontalSeparator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HorizontalSeparator.swift; sourceTree = ""; };
+ 556DC8B6487B074BB222CDA9 /* Counter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Counter.swift; sourceTree = ""; };
+ 559B640B10AF07E0E4755344 /* SearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = ""; };
56D23A73BBBCC7E6D4027CDB /* ExtraScenarios.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtraScenarios.swift; sourceTree = ""; };
- 5CC9723566343A9120E2BD3D /* SearchResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResult.swift; sourceTree = ""; };
- 621233CF50A0017066623146 /* ScenarioDisplayStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScenarioDisplayStore.swift; sourceTree = ""; };
- 636464CE7774B71958BDBCD2 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; };
66200EEB348D4A81C09138E5 /* Snapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Snapshot.swift; sourceTree = ""; };
+ 676CCC73E9C6DED60834DCF0 /* GalleryDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryDetail.swift; sourceTree = ""; };
68E1EB13B5ED8DD48245B1E8 /* ScenarioProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScenarioProvider.swift; sourceTree = ""; };
+ 698312DFD253858E0CE488E6 /* CatalogBottomBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogBottomBar.swift; sourceTree = ""; };
6B09750B60723BB88EB4E0BE /* SnapshotWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnapshotWindow.swift; sourceTree = ""; };
- 6D6EC7EA43758CD4E10A70A8 /* CatalogStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogStore.swift; sourceTree = ""; };
6E50413A08A9CF285D97A65C /* Scenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Scenario.swift; sourceTree = ""; };
- 70BEAC578E82D5EE82664DA2 /* SerialMainDispatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialMainDispatcher.swift; sourceTree = ""; };
+ 6ED586C95333C4B752C85423 /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = ""; };
+ 73329E8A55711E5737C66ACD /* SearchedKindData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchedKindData.swift; sourceTree = ""; };
77D5B1C44FC9E87FD3237459 /* ScenarioViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScenarioViewController.swift; sourceTree = ""; };
+ 7B2681920272C74A1032D3A4 /* ShareState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareState.swift; sourceTree = ""; };
+ 80C668330D834FEE619E6735 /* GalleryThumbnail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryThumbnail.swift; sourceTree = ""; };
81538A32C0A57BA96700E55E /* ScenarioStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScenarioStoreTests.swift; sourceTree = ""; };
+ 85A9B46FFAE9273D84C8A395 /* MaterialView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaterialView.swift; sourceTree = ""; };
8700BC5E0B042913C8C10CBA /* TestTool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestTool.swift; sourceTree = ""; };
+ 8908E9DA16AB4BA3A659914C /* CatalogState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogState.swift; sourceTree = ""; };
89B9EFFA662D997B8B95EE74 /* Export.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Export.swift; sourceTree = ""; };
- 8B7C02BA0BA547162205F277 /* SnapshotLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnapshotLoader.swift; sourceTree = ""; };
- 8EC4A04F778E41A014D8DD46 /* ScenarioContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScenarioContentView.swift; sourceTree = ""; };
9370D44977281AAEBF6DEF44 /* PlaybookTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybookTests.swift; sourceTree = ""; };
- 95BDCEF4FDC1DE7D50B6E73D /* Counter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Counter.swift; sourceTree = ""; };
- 95D5B5D2F570B29D57807A44 /* KeyboardDismissProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardDismissProxy.swift; sourceTree = ""; };
96A0C0FCA9FA7F91E01BB5A7 /* PlaybookSnapshot.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PlaybookSnapshot.framework; sourceTree = BUILT_PRODUCTS_DIR; };
97823B5F50AA1F1689DB8F56 /* ScenariosBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScenariosBuilder.swift; sourceTree = ""; };
+ 9A05522F96443D07E69B0DD3 /* ImageLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageLoader.swift; sourceTree = ""; };
+ 9A940D28E31EFAE5B3694CEE /* CatalogScenarioRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogScenarioRow.swift; sourceTree = ""; };
9B59B5A8B33031C6AF8A049E /* SnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnapshotTests.swift; sourceTree = ""; };
9BAD3FE8D577886981218938 /* GalleryScenarios.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryScenarios.swift; sourceTree = ""; };
- A2C24C0BB3864D3AF550A05D /* Scheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Scheduler.swift; sourceTree = ""; };
A5527163A80E0C62E2C9C19F /* ScenarioSwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScenarioSwiftUI.swift; sourceTree = ""; };
- A8FCF24068A7FDD9BAD27ECA /* ScenarioDisplaySheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScenarioDisplaySheet.swift; sourceTree = ""; };
- ABBE0D7EADE227862CB3EB1F /* TableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableView.swift; sourceTree = ""; };
+ AA6720986B0B52A13B30D528 /* ImageSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageSource.swift; sourceTree = ""; };
AEB78746E7DE13CB4BBD42FC /* Mocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mocks.swift; sourceTree = ""; };
+ B268BD03E4ADE4AE1F5BF269 /* CatalogKindRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogKindRow.swift; sourceTree = ""; };
+ B446C3A548A2DC847F032CA0 /* ScenarioContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScenarioContentView.swift; sourceTree = ""; };
+ B47949D75D9EE00D1B41E460 /* Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Image.swift; sourceTree = ""; };
B6C5F051089936C546DFBB95 /* SnapshotDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnapshotDevice.swift; sourceTree = ""; };
B6CEC4F17D84235922932498 /* ScenarioStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScenarioStore.swift; sourceTree = ""; };
- C65E03F37BA759FC6E55D1C2 /* Drawer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Drawer.swift; sourceTree = ""; };
+ BCBE2887B8D2BD7EA50CD94B /* HighlightText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightText.swift; sourceTree = ""; };
+ BEC649C8437116C1FFCA36D9 /* GalleryState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryState.swift; sourceTree = ""; };
+ C509DD2858530167BEC5BB11 /* SearchResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResult.swift; sourceTree = ""; };
+ C925FCA9E427334E3128E21D /* CatalogSplit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogSplit.swift; sourceTree = ""; };
CEB51C85B7D63CEF5DC83B60 /* ScenarioSwiftUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScenarioSwiftUITests.swift; sourceTree = ""; };
- CF2924992CEC971C20B6EC33 /* SearchedListData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchedListData.swift; sourceTree = ""; };
D15384CB93B122425863A487 /* CatalogScenarios.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogScenarios.swift; sourceTree = ""; };
+ D26212C25465F186B196A9E9 /* SearchState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchState.swift; sourceTree = ""; };
D36894DCD112EFAAEE670D92 /* ScenariosBuildable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScenariosBuildable.swift; sourceTree = ""; };
D63CD111722A3B8067BFE548 /* ScenarioContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScenarioContext.swift; sourceTree = ""; };
- DE2CD5659122DCA1911F566F /* Blur.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Blur.swift; sourceTree = ""; };
- DF07EECC1D23D59334517855 /* ScenarioDisplayList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScenarioDisplayList.swift; sourceTree = ""; };
- E2E59E35FC704B6D9414A6DC /* CatalogSplitStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogSplitStyle.swift; sourceTree = ""; };
- E955D716921B9CE34F4E35AC /* ImageSharingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageSharingView.swift; sourceTree = ""; };
+ DA2CB89FE909D51ACCC78465 /* GalleryDetailTopBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryDetailTopBar.swift; sourceTree = ""; };
+ DDF2169FF71AF568BF427357 /* UIColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIColor.swift; sourceTree = ""; };
EA762F5B447EF9828E1F86F2 /* PlaybookCatalog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybookCatalog.swift; sourceTree = ""; };
- EB7DE56F774FFD4DB73EBD37 /* CatalogBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogBarItem.swift; sourceTree = ""; };
EC23506D3CC07FABB1992787 /* Playbook-Tests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = "Playbook-Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
- F49057856849F915AB692364 /* ScenarioSearchStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScenarioSearchStore.swift; sourceTree = ""; };
+ ECE9B3C751E8D95A68E247DF /* GalleryKindRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryKindRow.swift; sourceTree = ""; };
F6B0431D91C939D181493ECC /* PlaybookGallery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybookGallery.swift; sourceTree = ""; };
F830987472F2FBD2C3BA7284 /* ScenarioKind.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScenarioKind.swift; sourceTree = ""; };
F913250C8A5AB7148459FEC6 /* ScenarioViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScenarioViewControllerTests.swift; sourceTree = ""; };
- FA6FEAB12579B7940EA763BB /* Highlight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Highlight.swift; sourceTree = ""; };
- FF4E86485B651D2EE3F6A2A6 /* EnvironmentValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentValues.swift; sourceTree = ""; };
FF835DB024B80E2FADFFD0FC /* ScenarioLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScenarioLayout.swift; sourceTree = ""; };
/* End PBXFileReference section */
@@ -321,37 +329,10 @@
6A92FC6CA051A044FE8B4F24 /* Internal */ = {
isa = PBXGroup;
children = (
- 1721044B445E0A9326CA6D21 /* Atomic.swift */,
- DE2CD5659122DCA1911F566F /* Blur.swift */,
- EB7DE56F774FFD4DB73EBD37 /* CatalogBarItem.swift */,
- 377D53C09A49B19955FA86BE /* CatalogDrawerStyle.swift */,
- E2E59E35FC704B6D9414A6DC /* CatalogSplitStyle.swift */,
- 6D6EC7EA43758CD4E10A70A8 /* CatalogStore.swift */,
- 95BDCEF4FDC1DE7D50B6E73D /* Counter.swift */,
- C65E03F37BA759FC6E55D1C2 /* Drawer.swift */,
- FF4E86485B651D2EE3F6A2A6 /* EnvironmentValues.swift */,
- 636464CE7774B71958BDBCD2 /* Extensions.swift */,
- 38F7F9F34307C4DEEDF210F9 /* GalleryStore.swift */,
- FA6FEAB12579B7940EA763BB /* Highlight.swift */,
- 5576F4CB67593B259E12C387 /* HorizontalSeparator.swift */,
- E955D716921B9CE34F4E35AC /* ImageSharingView.swift */,
- 95D5B5D2F570B29D57807A44 /* KeyboardDismissProxy.swift */,
- 8EC4A04F778E41A014D8DD46 /* ScenarioContentView.swift */,
- 39688046A26BC35A5FDFC672 /* ScenarioDisplay.swift */,
- DF07EECC1D23D59334517855 /* ScenarioDisplayList.swift */,
- A8FCF24068A7FDD9BAD27ECA /* ScenarioDisplaySheet.swift */,
- 621233CF50A0017066623146 /* ScenarioDisplayStore.swift */,
- F49057856849F915AB692364 /* ScenarioSearchStore.swift */,
- 068FE06929659477F04AA176 /* ScenarioSearchTree.swift */,
- A2C24C0BB3864D3AF550A05D /* Scheduler.swift */,
- 4351CB488B9D56ABCDD4E27A /* SearchBar.swift */,
- 369C805F890E0F0A27A44B5F /* SearchedData.swift */,
- CF2924992CEC971C20B6EC33 /* SearchedListData.swift */,
- 5CC9723566343A9120E2BD3D /* SearchResult.swift */,
- 70BEAC578E82D5EE82664DA2 /* SerialMainDispatcher.swift */,
- 8B7C02BA0BA547162205F277 /* SnapshotLoader.swift */,
- ABBE0D7EADE227862CB3EB1F /* TableView.swift */,
- 1CED8582C39C01B51301E1CF /* WeakReference.swift */,
+ 9F0631225653E9350F11DD57 /* Entities */,
+ EC193057B062B7F025F74131 /* State */,
+ FECB4EC25441B5B6E1EC4415 /* Utilities */,
+ BCB10B60C3EF6BAD57782CAD /* Views */,
);
path = Internal;
sourceTree = "";
@@ -381,6 +362,43 @@
path = Internal;
sourceTree = "";
};
+ 9F0631225653E9350F11DD57 /* Entities */ = {
+ isa = PBXGroup;
+ children = (
+ 2F412670B8570C6C62C7875E /* SearchedData.swift */,
+ 73329E8A55711E5737C66ACD /* SearchedKindData.swift */,
+ C509DD2858530167BEC5BB11 /* SearchResult.swift */,
+ 2532CFEE0AB6105AEB5B74E2 /* SelectData.swift */,
+ );
+ path = Entities;
+ sourceTree = "";
+ };
+ BCB10B60C3EF6BAD57782CAD /* Views */ = {
+ isa = PBXGroup;
+ children = (
+ 698312DFD253858E0CE488E6 /* CatalogBottomBar.swift */,
+ 3FC40B940F3E03A47FEC3EC8 /* CatalogDrawer.swift */,
+ B268BD03E4ADE4AE1F5BF269 /* CatalogKindRow.swift */,
+ 9A940D28E31EFAE5B3694CEE /* CatalogScenarioRow.swift */,
+ 028BA6B86229E5CB18B406C1 /* CatalogSearchPane.swift */,
+ C925FCA9E427334E3128E21D /* CatalogSplit.swift */,
+ 4846AD7448E08DBA3E2C18A5 /* CatalogTop.swift */,
+ 2F44D4BCEA15399D487993CA /* ColorSchemePicker.swift */,
+ 556DC8B6487B074BB222CDA9 /* Counter.swift */,
+ 676CCC73E9C6DED60834DCF0 /* GalleryDetail.swift */,
+ DA2CB89FE909D51ACCC78465 /* GalleryDetailTopBar.swift */,
+ ECE9B3C751E8D95A68E247DF /* GalleryKindRow.swift */,
+ 80C668330D834FEE619E6735 /* GalleryThumbnail.swift */,
+ BCBE2887B8D2BD7EA50CD94B /* HighlightText.swift */,
+ 85A9B46FFAE9273D84C8A395 /* MaterialView.swift */,
+ B446C3A548A2DC847F032CA0 /* ScenarioContentView.swift */,
+ 559B640B10AF07E0E4755344 /* SearchBar.swift */,
+ 11067B4E4A9D6494867D706A /* Separator.swift */,
+ 29B5E679D86934A2FB5A0856 /* UnavailableView.swift */,
+ );
+ path = Views;
+ sourceTree = "";
+ };
C71D05311B2E181F61A0F029 /* Tests */ = {
isa = PBXGroup;
children = (
@@ -409,6 +427,32 @@
path = SnapshotSupport;
sourceTree = "";
};
+ EC193057B062B7F025F74131 /* State */ = {
+ isa = PBXGroup;
+ children = (
+ 8908E9DA16AB4BA3A659914C /* CatalogState.swift */,
+ BEC649C8437116C1FFCA36D9 /* GalleryState.swift */,
+ D26212C25465F186B196A9E9 /* SearchState.swift */,
+ 7B2681920272C74A1032D3A4 /* ShareState.swift */,
+ );
+ path = State;
+ sourceTree = "";
+ };
+ FECB4EC25441B5B6E1EC4415 /* Utilities */ = {
+ isa = PBXGroup;
+ children = (
+ B47949D75D9EE00D1B41E460 /* Image.swift */,
+ 2B12B3F422F9EE7600CA7B8C /* ImageCache.swift */,
+ 9A05522F96443D07E69B0DD3 /* ImageLoader.swift */,
+ AA6720986B0B52A13B30D528 /* ImageSource.swift */,
+ 4CEE9E33C79F43CEDD6BA712 /* Spacer.swift */,
+ DDF2169FF71AF568BF427357 /* UIColor.swift */,
+ 6ED586C95333C4B752C85423 /* UIImage.swift */,
+ 391B584F5DA7B584216E4619 /* View.swift */,
+ );
+ path = Utilities;
+ sourceTree = "";
+ };
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -581,40 +625,44 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- 80BBDCCF2C1580F0F508ED73 /* Atomic.swift in Sources */,
- E1680C7D25ADB69BF62A6BF5 /* Blur.swift in Sources */,
- 795BBCD562B512FC4ED96448 /* CatalogBarItem.swift in Sources */,
- B4384D25712D57EBEDE149F6 /* CatalogDrawerStyle.swift in Sources */,
- A3236C9B14BBF8D34C16BC45 /* CatalogSplitStyle.swift in Sources */,
- 25C326C9B1CBC90FC989C264 /* CatalogStore.swift in Sources */,
- 0E1A77DB134CE41112BE7EFB /* Counter.swift in Sources */,
- B89DF49D830D61DA0B3C084D /* Drawer.swift in Sources */,
- F68746E7D5745E764F19D377 /* EnvironmentValues.swift in Sources */,
+ EE5F75875FDD118B4A5F0BF8 /* CatalogBottomBar.swift in Sources */,
+ AD7EFE7D41A4801FEABBF156 /* CatalogDrawer.swift in Sources */,
+ DE001E8A450A047BE1F2AB7C /* CatalogKindRow.swift in Sources */,
+ 4C55A5EE142078443D368124 /* CatalogScenarioRow.swift in Sources */,
+ FB311A5E12E4B63A845D274A /* CatalogSearchPane.swift in Sources */,
+ 8C596670DA8078B4CA2AA743 /* CatalogSplit.swift in Sources */,
+ EC542FAB97124C8728AE06DD /* CatalogState.swift in Sources */,
+ 7AF75286918869ED0516B47D /* CatalogTop.swift in Sources */,
+ 14CB92EA193DE1EEB60DC590 /* ColorSchemePicker.swift in Sources */,
+ B257A455408A87DD7A0D9F6B /* Counter.swift in Sources */,
509D6D27E6A76DCBDEE64226 /* Export.swift in Sources */,
- 1561DDA840821DF8807FF006 /* Extensions.swift in Sources */,
- D45CD95A63FC59D24E4F5E65 /* GalleryStore.swift in Sources */,
- 0FEB2071F01AF72A62527F63 /* Highlight.swift in Sources */,
- C0F32DC787F41414E09C37FB /* HorizontalSeparator.swift in Sources */,
- A28E3CC1C92EE68DEE3A627E /* ImageSharingView.swift in Sources */,
- 97F1D39B80BA6FC503E6C1DB /* KeyboardDismissProxy.swift in Sources */,
+ EC7FB83606201C8AAA15D098 /* GalleryDetail.swift in Sources */,
+ FE054384773081FAFD2F3A55 /* GalleryDetailTopBar.swift in Sources */,
+ 8298234AB8AC2B9046BE09D0 /* GalleryKindRow.swift in Sources */,
+ C57746152ACAC8D242E7992A /* GalleryState.swift in Sources */,
+ 3D179776ADD757235527A929 /* GalleryThumbnail.swift in Sources */,
+ 17740091AF2876BEA30F0E68 /* HighlightText.swift in Sources */,
+ F7D7D9ADA3D68C3C17B9968D /* Image.swift in Sources */,
+ 4104FE671C498D67FB0134C3 /* ImageCache.swift in Sources */,
+ 99A112D26891F0ED999FE27B /* ImageLoader.swift in Sources */,
+ B44C35245CFBC0043183D67C /* ImageSource.swift in Sources */,
+ 1F6EB9E17EC82DFFE52C8FE9 /* MaterialView.swift in Sources */,
668D80F30C0A09A5667C592A /* PlaybookCatalog.swift in Sources */,
2C4D9ABC6FC9696E42BE8080 /* PlaybookGallery.swift in Sources */,
- 6BC169CC10D3F962E48C7A60 /* ScenarioContentView.swift in Sources */,
- 9C10E142F8D14B64C893B9BD /* ScenarioDisplay.swift in Sources */,
- 59BC00DE37B8F0ED2324F9E6 /* ScenarioDisplayList.swift in Sources */,
- 4F942C3581B6150EC113B378 /* ScenarioDisplaySheet.swift in Sources */,
- 2336557C8E97894E4737A528 /* ScenarioDisplayStore.swift in Sources */,
- 88C7D7A743A333E7DF642F54 /* ScenarioSearchStore.swift in Sources */,
- C6A0A71A7D1DB2BECD5B4A2D /* ScenarioSearchTree.swift in Sources */,
- 0D0659EBE8EF0971AF40405D /* Scheduler.swift in Sources */,
- FAB1302DAAB4D0C5CF83C1C1 /* SearchBar.swift in Sources */,
- 663722E1FA2A87F28409489D /* SearchResult.swift in Sources */,
- 819E05B5E7ABEB948250AD8C /* SearchedData.swift in Sources */,
- 1045A54325FE670A3EC64866 /* SearchedListData.swift in Sources */,
- 2835B28061CBB1F908CE8D14 /* SerialMainDispatcher.swift in Sources */,
- 3DFE3E1AF8E3691457DF156C /* SnapshotLoader.swift in Sources */,
- 5D84ECFA16954ACC9A0E1141 /* TableView.swift in Sources */,
- FF4469309B532A58B10C5EE1 /* WeakReference.swift in Sources */,
+ AE0DE7D74E721B0D254353B2 /* ScenarioContentView.swift in Sources */,
+ 0A26157EE6E7B940067CA211 /* SearchBar.swift in Sources */,
+ 08DB4240FAF948F16398C1C9 /* SearchResult.swift in Sources */,
+ 56FFC73C5EA4C3F7AB583136 /* SearchState.swift in Sources */,
+ F16829BEF3BB1173FFFA18B0 /* SearchedData.swift in Sources */,
+ D97CC8D3208EEDF6B11EF898 /* SearchedKindData.swift in Sources */,
+ 5494FCBB0627B107CAF044DB /* SelectData.swift in Sources */,
+ 8DDAA3EF4EB3E70B9360432F /* Separator.swift in Sources */,
+ B1B7C962BA824C7739DD7486 /* ShareState.swift in Sources */,
+ 71721E7FA3A9FC3017EEA4FC /* Spacer.swift in Sources */,
+ 9CBF0652A467E45FB7187988 /* UIColor.swift in Sources */,
+ 4D66B6CDF20B4B1C3DEF6790 /* UIImage.swift in Sources */,
+ 748569229F0F50D9C36EE2E9 /* UnavailableView.swift in Sources */,
+ A1CD039E4B6D017210DEFF0E /* View.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
diff --git a/Sources/Playbook/SnapshotSupport/Internal/SnapshotWindow.swift b/Sources/Playbook/SnapshotSupport/Internal/SnapshotWindow.swift
index d2818ca..249f967 100644
--- a/Sources/Playbook/SnapshotSupport/Internal/SnapshotWindow.swift
+++ b/Sources/Playbook/SnapshotSupport/Internal/SnapshotWindow.swift
@@ -47,14 +47,14 @@ internal final class SnapshotWindow: UIWindow {
scenarioViewController.disablesEndAppearanceTransition = true
scenarioViewController.shouldStatusBarHidden = true
- frame.size = CGSize(
- width: scenario.layout.fixedWidth ?? device.size.width,
- height: scenario.layout.fixedHeight ?? device.size.height
- )
windowLevel = .normal - 1
layer.speed = .greatestFiniteMagnitude
rootViewController = scenarioViewController
isHidden = false
+ frame.size = CGSize(
+ width: scenario.layout.fixedWidth ?? device.size.width,
+ height: scenario.layout.fixedHeight ?? device.size.height
+ )
if window != nil {
// In iOS 16 and 17, setting `isHidden` nor does not update
diff --git a/Sources/PlaybookUI/Internal/Atomic.swift b/Sources/PlaybookUI/Internal/Atomic.swift
deleted file mode 100644
index 8f38a19..0000000
--- a/Sources/PlaybookUI/Internal/Atomic.swift
+++ /dev/null
@@ -1,32 +0,0 @@
-import Foundation
-
-@propertyWrapper
-internal final class Atomic {
- var wrappedValue: Value {
- get { modify { $0 } }
- set { modify { $0 = newValue } }
- }
-
- private let lock = NSLock()
- private var value: Value
-
- init(wrappedValue: Value) {
- self.value = wrappedValue
- }
-
- @discardableResult
- func modify(action: (inout Value) -> T) -> T {
- lock.lock()
- defer { lock.unlock() }
- return action(&value)
- }
-
- @discardableResult
- func swap(_ newValue: Value) -> Value {
- modify { value in
- let oldValue = value
- value = newValue
- return oldValue
- }
- }
-}
diff --git a/Sources/PlaybookUI/Internal/Blur.swift b/Sources/PlaybookUI/Internal/Blur.swift
deleted file mode 100644
index b695a4d..0000000
--- a/Sources/PlaybookUI/Internal/Blur.swift
+++ /dev/null
@@ -1,13 +0,0 @@
-import SwiftUI
-
-internal struct Blur: UIViewRepresentable {
- var style: UIBlurEffect.Style
-
- func makeUIView(context: Context) -> UIVisualEffectView {
- UIVisualEffectView()
- }
-
- func updateUIView(_ uiView: UIVisualEffectView, context: Context) {
- uiView.effect = UIBlurEffect(style: style)
- }
-}
diff --git a/Sources/PlaybookUI/Internal/CatalogBarItem.swift b/Sources/PlaybookUI/Internal/CatalogBarItem.swift
deleted file mode 100644
index 860f3b8..0000000
--- a/Sources/PlaybookUI/Internal/CatalogBarItem.swift
+++ /dev/null
@@ -1,17 +0,0 @@
-import SwiftUI
-
-internal struct CatalogBarItem: View {
- var image: Image
- var insets: EdgeInsets
- var action: () -> Void
-
- var body: some View {
- Button(action: action) {
- image
- .padding(8)
- .imageScale(.large)
- .foregroundColor(Color(.label))
- .padding(insets)
- }
- }
-}
diff --git a/Sources/PlaybookUI/Internal/CatalogDrawerStyle.swift b/Sources/PlaybookUI/Internal/CatalogDrawerStyle.swift
deleted file mode 100644
index 8a71ac0..0000000
--- a/Sources/PlaybookUI/Internal/CatalogDrawerStyle.swift
+++ /dev/null
@@ -1,41 +0,0 @@
-import SwiftUI
-
-internal struct CatalogDrawerStyle: View {
- var name: String
- var searchTree: ScenarioSearchTree
- var content: (CatalogBarItem) -> Content
-
- @EnvironmentObject
- private var store: CatalogStore
-
- public init(
- name: String,
- searchTree: ScenarioSearchTree,
- content: @escaping (CatalogBarItem) -> Content
- ) {
- self.name = name
- self.searchTree = searchTree
- self.content = content
- }
-
- var body: some View {
- ZStack {
- content(
- CatalogBarItem(image: Image(symbol: .magnifyingglass), insets: .only(top: 2)) {
- self.store.isSearchTreeHidden = false
- }
- )
-
- Drawer(isOpened: isOpened, content: searchTree)
- }
- }
-}
-
-private extension CatalogDrawerStyle {
- var isOpened: Binding {
- Binding(
- get: { !self.store.isSearchTreeHidden },
- set: { self.store.isSearchTreeHidden = !$0 }
- )
- }
-}
diff --git a/Sources/PlaybookUI/Internal/CatalogSplitStyle.swift b/Sources/PlaybookUI/Internal/CatalogSplitStyle.swift
deleted file mode 100644
index 0a6e6b8..0000000
--- a/Sources/PlaybookUI/Internal/CatalogSplitStyle.swift
+++ /dev/null
@@ -1,59 +0,0 @@
-import SwiftUI
-
-internal struct CatalogSplitStyle: View {
- var name: String
- var searchTree: ScenarioSearchTree
- var content: (CatalogBarItem) -> Content
-
- @EnvironmentObject
- private var store: CatalogStore
-
- public init(
- name: String,
- searchTree: ScenarioSearchTree,
- content: @escaping (CatalogBarItem) -> Content
- ) {
- self.name = name
- self.searchTree = searchTree
- self.content = content
- }
-
- var body: some View {
- GeometryReader { geometry in
- ZStack(alignment: .leading) {
- HStack(spacing: 0) {
- self.searchTree
-
- Divider()
- .edgesIgnoringSafeArea(.all)
- }
- .frame(width: self.sidebarWidth(with: geometry))
-
- HStack(spacing: 0) {
- Spacer.fixed(length: self.store.isSearchTreeHidden ? 0 : self.sidebarWidth(with: geometry))
-
- self.content(
- CatalogBarItem(
- image: Image(symbol: self.store.isSearchTreeHidden ? .sidebarLeft : .rectangle),
- insets: .only(top: 2),
- action: { self.store.isSearchTreeHidden.toggle() }
- )
- )
- }
- }
- .animation(nil, value: geometry.size)
- .animation(.interactiveSpring())
- .transformEnvironment(\.horizontalSizeClass) { sizeClass in
- if !self.store.isSearchTreeHidden && geometry.size.width < geometry.size.height {
- sizeClass = .compact
- }
- }
- }
- }
-}
-
-private extension CatalogSplitStyle {
- func sidebarWidth(with geometry: GeometryProxy) -> CGFloat {
- geometry.size.width / 3
- }
-}
diff --git a/Sources/PlaybookUI/Internal/CatalogStore.swift b/Sources/PlaybookUI/Internal/CatalogStore.swift
deleted file mode 100644
index b7a141d..0000000
--- a/Sources/PlaybookUI/Internal/CatalogStore.swift
+++ /dev/null
@@ -1,37 +0,0 @@
-internal final class CatalogStore: ScenarioSearchStore {
- var selectedScenario: SearchedData? {
- willSet { objectWillChange.send() }
- }
-
- var openedKinds = Set() {
- willSet { objectWillChange.send() }
- }
-
- var openedSearchingKinds: Set? {
- willSet { objectWillChange.send() }
- }
-
- var shareItem: ImageSharingView.Item? {
- willSet { objectWillChange.send() }
- }
-
- var isSearchTreeHidden = false {
- willSet { objectWillChange.send() }
- }
-
- init(
- playbook: Playbook,
- selectedScenario: SearchedData? = nil,
- openedKinds: Set = [],
- openedSearchingKinds: Set? = nil,
- shareItem: ImageSharingView.Item? = nil,
- isSearchTreeHidden: Bool = false
- ) {
- self.selectedScenario = selectedScenario
- self.openedKinds = openedKinds
- self.openedSearchingKinds = openedSearchingKinds
- self.shareItem = shareItem
- self.isSearchTreeHidden = isSearchTreeHidden
- super.init(playbook: playbook)
- }
-}
diff --git a/Sources/PlaybookUI/Internal/Counter.swift b/Sources/PlaybookUI/Internal/Counter.swift
deleted file mode 100644
index 3612f3a..0000000
--- a/Sources/PlaybookUI/Internal/Counter.swift
+++ /dev/null
@@ -1,19 +0,0 @@
-import SwiftUI
-
-internal struct Counter: View, Equatable {
- var numerator: Int
- var denominator: Int
-
- var body: some View {
- HStack(spacing: 0) {
- Spacer.zero
-
- Text("\(numerator) / \(denominator)")
- .font(Font.subheadline.monospacedDigit())
- .foregroundColor(Color(.label))
- }
- .padding(.top, 8)
- .padding(.horizontal, 24)
- .animation(nil, value: self)
- }
-}
diff --git a/Sources/PlaybookUI/Internal/Drawer.swift b/Sources/PlaybookUI/Internal/Drawer.swift
deleted file mode 100644
index fdbd9bd..0000000
--- a/Sources/PlaybookUI/Internal/Drawer.swift
+++ /dev/null
@@ -1,112 +0,0 @@
-import SwiftUI
-
-internal struct Drawer: View {
- @Binding
- var isOpened: Bool
-
- var content: Content
-
- @State
- private var dragState = DragState.inactive
-
- private var keyboardProxy = KeyboardDismissProxy()
-
- init(isOpened: Binding, content: Content) {
- self._isOpened = isOpened
- self.content = content
- }
-
- var body: some View {
- GeometryReader { geometry in
- ZStack(alignment: .topLeading) {
- Color.black
- .opacity(self.openingRatio(with: geometry) * 0.3)
- .edgesIgnoringSafeArea(.all)
- .onTapGesture(perform: self.close)
-
- HStack(alignment: .top, spacing: 0) {
- self.content
- .background(self.withShadow(Color.black.edgesIgnoringSafeArea(.all)))
- .frame(width: self.drawerWidth(with: geometry))
-
- self.withShadow(
- Button(action: self.close) {
- Image(symbol: .multiply)
- .imageScale(.medium)
- .font(Font.title.bold())
- .foregroundColor(.white)
- .padding(16)
- }
- .opacity(self.openingRatio(with: geometry))
- .allowsHitTesting(self.isOpened)
- )
-
- Spacer.zero
- }
- .offset(x: self.offset(with: geometry))
- }
- .animation(nil, value: geometry.size)
- .animation(.interactiveSpring())
- .frame(width: geometry.size.width, height: geometry.size.height)
- .gesture(self.dragGesture(with: geometry))
- .dismissKeyboard(by: self.keyboardProxy)
- }
- }
-}
-
-private extension Drawer {
- enum DragState {
- case inactive
- case dragging(translation: CGFloat)
- }
-
- var shadowRadius: CGFloat { 12 }
-
- func withShadow(_ content: some View) -> some View {
- content.shadow(color: Color.black.opacity(0.3), radius: shadowRadius)
- }
-
- func dragGesture(with geometry: GeometryProxy) -> some Gesture {
- let gesture = DragGesture(coordinateSpace: .global)
- .onChanged { drag in
- let isHorizontalDrag = abs(drag.translation.width) >= abs(drag.translation.height)
- self.dragState = isHorizontalDrag ? .dragging(translation: drag.translation.width) : .inactive
- self.keyboardProxy.dismissKeyboard()
- }
- .onEnded { drag in
- let ratio = min(1, max(0, -drag.predictedEndTranslation.width / self.drawerWidth(with: geometry)))
- self.isOpened = ratio <= 0.5
- self.dragState = .inactive
- }
- return isOpened ? gesture : nil
- }
-
- func close() {
- dragState = .inactive
- isOpened = false
- keyboardProxy.dismissKeyboard()
- }
-
- func drawerWidth(with geometry: GeometryProxy) -> CGFloat {
- geometry.safeAreaInsets.leading + min(geometry.size.width, geometry.size.height) * 0.8
- }
-
- func hiddenOffset(with geometry: GeometryProxy) -> CGFloat {
- -drawerWidth(with: geometry) - geometry.safeAreaInsets.leading - shadowRadius
- }
-
- func offset(with geometry: GeometryProxy) -> CGFloat {
- CGFloat(1 - openingRatio(with: geometry)) * hiddenOffset(with: geometry)
- }
-
- func openingRatio(with geometry: GeometryProxy) -> Double {
- switch dragState {
- case .inactive:
- return isOpened ? 1 : 0
-
- case .dragging(let translation):
- let ratio = translation / drawerWidth(with: geometry)
- return Double(min(1, max(0, isOpened ? 1 + ratio : ratio)))
- }
- }
-}
diff --git a/Sources/PlaybookUI/Internal/Entities/SearchResult.swift b/Sources/PlaybookUI/Internal/Entities/SearchResult.swift
new file mode 100644
index 0000000..c05948e
--- /dev/null
+++ b/Sources/PlaybookUI/Internal/Entities/SearchResult.swift
@@ -0,0 +1,5 @@
+internal struct SearchResult {
+ let count: Int
+ let total: Int
+ let kinds: [SearchedKindData]
+}
diff --git a/Sources/PlaybookUI/Internal/Entities/SearchedData.swift b/Sources/PlaybookUI/Internal/Entities/SearchedData.swift
new file mode 100644
index 0000000..1388ce6
--- /dev/null
+++ b/Sources/PlaybookUI/Internal/Entities/SearchedData.swift
@@ -0,0 +1,7 @@
+import Playbook
+
+internal struct SearchedData {
+ let kind: ScenarioKind
+ let scenario: Scenario
+ let highlightRange: Range?
+}
diff --git a/Sources/PlaybookUI/Internal/Entities/SearchedKindData.swift b/Sources/PlaybookUI/Internal/Entities/SearchedKindData.swift
new file mode 100644
index 0000000..a011b59
--- /dev/null
+++ b/Sources/PlaybookUI/Internal/Entities/SearchedKindData.swift
@@ -0,0 +1,7 @@
+import Playbook
+
+internal struct SearchedKindData {
+ let kind: ScenarioKind
+ let highlightRange: Range?
+ let scenarios: [SearchedData]
+}
diff --git a/Sources/PlaybookUI/Internal/Entities/SelectData.swift b/Sources/PlaybookUI/Internal/Entities/SelectData.swift
new file mode 100644
index 0000000..dc27fa5
--- /dev/null
+++ b/Sources/PlaybookUI/Internal/Entities/SelectData.swift
@@ -0,0 +1,15 @@
+import Playbook
+
+internal struct SelectData: Identifiable {
+ struct ID: Hashable {
+ let kind: ScenarioKind
+ let name: ScenarioName
+ }
+
+ var id: ID {
+ ID(kind: kind, name: scenario.name)
+ }
+
+ let kind: ScenarioKind
+ let scenario: Scenario
+}
diff --git a/Sources/PlaybookUI/Internal/EnvironmentValues.swift b/Sources/PlaybookUI/Internal/EnvironmentValues.swift
deleted file mode 100644
index 04eb6c4..0000000
--- a/Sources/PlaybookUI/Internal/EnvironmentValues.swift
+++ /dev/null
@@ -1,31 +0,0 @@
-import SwiftUI
-
-internal struct GalleryDependency {
- var scheduler: SchedulerProtocol
- var context: ScenarioContext
-
- init(scheduler: SchedulerProtocol, context: ScenarioContext) {
- self.scheduler = scheduler
- self.context = context
- }
-}
-
-internal enum GalleryDependencyEnvironmentKey: EnvironmentKey {
- static var defaultValue: GalleryDependency {
- GalleryDependency(
- scheduler: Scheduler(),
- context: ScenarioContext(
- snapshotWaiter: SnapshotWaiter(),
- isSnapshot: false,
- screenSize: UIScreen.main.bounds.size
- )
- )
- }
-}
-
-internal extension EnvironmentValues {
- var galleryDependency: GalleryDependency {
- get { self[GalleryDependencyEnvironmentKey.self] }
- set { self[GalleryDependencyEnvironmentKey.self] = newValue }
- }
-}
diff --git a/Sources/PlaybookUI/Internal/Extensions.swift b/Sources/PlaybookUI/Internal/Extensions.swift
deleted file mode 100644
index 96466bc..0000000
--- a/Sources/PlaybookUI/Internal/Extensions.swift
+++ /dev/null
@@ -1,86 +0,0 @@
-import SwiftUI
-
-internal extension UIColor {
- static let primaryBlue = UIColor(hex: 0x048DFF)
-
- static let primaryBackground = UIColor { traitCollection in
- traitCollection.userInterfaceStyle == .light ? .white : UIColor(hex: 0x18242D)
- }
-
- static let secondaryBackground = UIColor { traitCollection in
- traitCollection.userInterfaceStyle == .light ? .white : UIColor(hex: 0x29373F)
- }
-
- static let scenarioBackground = UIColor { traitCollection in
- traitCollection.userInterfaceStyle == .light ? .white : .black
- }
-
- static let highlight = UIColor.systemYellow.withAlphaComponent(0.4)
-
- convenience init(hex: Int, alpha: CGFloat = 1) {
- let red = CGFloat((hex & 0xFF0000) >> 16) / 255
- let green = CGFloat((hex & 0x00FF00) >> 8) / 255
- let blue = CGFloat((hex & 0x0000FF) >> 0) / 255
-
- self.init(red: red, green: green, blue: blue, alpha: alpha)
- }
-
- func circleImage(length: CGFloat) -> UIImage {
- let size = CGSize(width: length, height: length)
- return UIGraphicsImageRenderer(size: size).image { context in
- let rect = CGRect(origin: .zero, size: size)
- setFill()
- context.cgContext.fillEllipse(in: rect)
- }
- }
-}
-
-internal extension Image {
- enum SFSymbols: String {
- case book = "book"
- case multiply = "multiply"
- case chevronRight = "chevron.right"
- case bookmarkFill = "bookmark.fill"
- case circleFill = "circle.fill"
- case sidebarLeft = "sidebar.left"
- case rectangle = "rectangle"
- case squareAndArrowUp = "square.and.arrow.up"
- case magnifyingglass = "magnifyingglass"
- }
-
- init(symbol: SFSymbols) {
- self.init(systemName: symbol.rawValue)
- }
-}
-
-internal extension Spacer {
- static var zero: Spacer {
- Spacer(minLength: 0)
- }
-
- static func fixed(length: CGFloat) -> some View {
- Spacer(minLength: length).fixedSize()
- }
-}
-
-internal extension EdgeInsets {
- static func only(
- top: CGFloat = 0,
- leading: CGFloat = 0,
- bottom: CGFloat = 0,
- trailing: CGFloat = 0
- ) -> EdgeInsets {
- EdgeInsets(top: top, leading: leading, bottom: bottom, trailing: trailing)
- }
-}
-
-internal extension UIEdgeInsets {
- static func only(
- top: CGFloat = 0,
- left: CGFloat = 0,
- bottom: CGFloat = 0,
- right: CGFloat = 0
- ) -> UIEdgeInsets {
- UIEdgeInsets(top: top, left: left, bottom: bottom, right: right)
- }
-}
diff --git a/Sources/PlaybookUI/Internal/GalleryStore.swift b/Sources/PlaybookUI/Internal/GalleryStore.swift
deleted file mode 100644
index 46a14a0..0000000
--- a/Sources/PlaybookUI/Internal/GalleryStore.swift
+++ /dev/null
@@ -1,96 +0,0 @@
-import SwiftUI
-
-internal final class GalleryStore: ScenarioSearchStore {
- enum Status {
- case standby
- case ready
- }
-
- var selectedScenario: SearchedData? {
- willSet { objectWillChange.send() }
- }
-
- var status = Status.standby {
- willSet { objectWillChange.send() }
- }
-
- var shareItem: ImageSharingView.Item? {
- willSet { objectWillChange.send() }
- }
-
- let preSnapshotCountLimit: Int
- let snapshotLoader: SnapshotLoaderProtocol
-
- func prepare() {
- switch status {
- case .ready:
- break
-
- case .standby:
- takeSnapshots()
- start()
- status = .ready
- }
- }
-
- @discardableResult
- func takeSnapshots() -> Self {
- snapshotLoader.clean()
-
- playbook.stores.lazy
- .flatMap { store in
- store.scenarios.map { scenario in
- (kind: store.kind, scenario: scenario)
- }
- }
- .prefix(preSnapshotCountLimit)
- .forEach { snapshotLoader.takeSnapshot(for: $0.scenario, kind: $0.kind, completion: nil) }
-
- return self
- }
-
- init(
- playbook: Playbook,
- preSnapshotCountLimit: Int,
- selectedScenario: SearchedData? = nil,
- status: Status = .standby,
- shareItem: ImageSharingView.Item? = nil,
- snapshotLoader: SnapshotLoaderProtocol
- ) {
- self.preSnapshotCountLimit = preSnapshotCountLimit
- self.snapshotLoader = snapshotLoader
- self.selectedScenario = selectedScenario
- self.status = status
- self.shareItem = shareItem
-
- super.init(playbook: playbook)
- }
-
- convenience init(
- playbook: Playbook,
- preSnapshotCountLimit: Int,
- screenSize: CGSize,
- userInterfaceStyle: UIUserInterfaceStyle,
- selectedScenario: SearchedData? = nil,
- status: Status = .standby,
- shareItem: ImageSharingView.Item? = nil
- ) {
- self.init(
- playbook: playbook,
- preSnapshotCountLimit: preSnapshotCountLimit,
- selectedScenario: selectedScenario,
- status: status,
- shareItem: shareItem,
- snapshotLoader: SnapshotLoader(
- name: "app.playbook-ui.SnapshotLoader",
- baseDirectoryURL: URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true),
- format: .png,
- device: SnapshotDevice(
- name: "PlaybookCatalog",
- size: screenSize,
- traitCollection: UITraitCollection(userInterfaceStyle: userInterfaceStyle)
- )
- )
- )
- }
-}
diff --git a/Sources/PlaybookUI/Internal/Highlight.swift b/Sources/PlaybookUI/Internal/Highlight.swift
deleted file mode 100644
index cf4fe79..0000000
--- a/Sources/PlaybookUI/Internal/Highlight.swift
+++ /dev/null
@@ -1,17 +0,0 @@
-import SwiftUI
-
-internal struct Highlight: View {
- var isHighlighted: Bool
-
- init(_ isHighlighted: Bool) {
- self.isHighlighted = isHighlighted
- }
-
- var body: some View {
- Group {
- if isHighlighted {
- Color(.highlight)
- }
- }
- }
-}
diff --git a/Sources/PlaybookUI/Internal/HorizontalSeparator.swift b/Sources/PlaybookUI/Internal/HorizontalSeparator.swift
deleted file mode 100644
index ecdd9eb..0000000
--- a/Sources/PlaybookUI/Internal/HorizontalSeparator.swift
+++ /dev/null
@@ -1,10 +0,0 @@
-import SwiftUI
-
-internal struct HorizontalSeparator: View {
- var body: some View {
- Rectangle()
- .fill(Color(.quaternarySystemFill))
- .frame(height: 2)
- .fixedSize(horizontal: false, vertical: true)
- }
-}
diff --git a/Sources/PlaybookUI/Internal/ImageSharingView.swift b/Sources/PlaybookUI/Internal/ImageSharingView.swift
deleted file mode 100644
index e469d57..0000000
--- a/Sources/PlaybookUI/Internal/ImageSharingView.swift
+++ /dev/null
@@ -1,35 +0,0 @@
-import SwiftUI
-
-internal struct ImageSharingView: UIViewControllerRepresentable {
- struct Item: Identifiable {
- var id: Int { image.hash }
- var image: UIImage
- }
-
- var item: Item
- var onComplete: () -> Void
-
- func makeUIViewController(context: Context) -> UIActivityViewController {
- let uiViewController = UIActivityViewController(
- activityItems: [item.image],
- applicationActivities: nil
- )
- if !Bundle.main.hasPhotoLibraryAddUsageDescription {
- uiViewController.excludedActivityTypes = [.saveToCameraRoll]
- }
- return uiViewController
- }
-
- func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {
- uiViewController.completionWithItemsHandler = { _, _, _, _ in
- self.onComplete()
- }
- }
-}
-
-private extension Bundle {
- var hasPhotoLibraryAddUsageDescription: Bool {
- let usage = object(forInfoDictionaryKey: "NSPhotoLibraryAddUsageDescription") as? String
- return usage.map { !$0.isEmpty } ?? false
- }
-}
diff --git a/Sources/PlaybookUI/Internal/KeyboardDismissProxy.swift b/Sources/PlaybookUI/Internal/KeyboardDismissProxy.swift
deleted file mode 100644
index 332cb0a..0000000
--- a/Sources/PlaybookUI/Internal/KeyboardDismissProxy.swift
+++ /dev/null
@@ -1,43 +0,0 @@
-import SwiftUI
-
-internal final class KeyboardDismissProxy {
- fileprivate weak var uiView: UIView?
-
- func dismissKeyboard() {
- uiView?.window?.endEditing(true)
- }
-
- init() {}
-}
-
-internal extension View {
- func dismissKeyboard(by proxy: KeyboardDismissProxy) -> some View {
- background(
- HiddenView(proxy: proxy)
- .frame(width: 0, height: 0)
- .fixedSize()
- .opacity(0)
- .allowsHitTesting(false)
- )
- }
-}
-
-private struct HiddenView: UIViewRepresentable {
- var proxy: KeyboardDismissProxy
-
- init(proxy: KeyboardDismissProxy) {
- self.proxy = proxy
- }
-
- func makeUIView(context: Context) -> UIView {
- let uiView = UIView()
- uiView.backgroundColor = .clear
- uiView.isHidden = true
- uiView.isUserInteractionEnabled = false
- return uiView
- }
-
- func updateUIView(_ uiView: UIView, context: Context) {
- proxy.uiView = uiView
- }
-}
diff --git a/Sources/PlaybookUI/Internal/ScenarioContentView.swift b/Sources/PlaybookUI/Internal/ScenarioContentView.swift
deleted file mode 100644
index 93dc8ea..0000000
--- a/Sources/PlaybookUI/Internal/ScenarioContentView.swift
+++ /dev/null
@@ -1,47 +0,0 @@
-import SwiftUI
-
-internal struct ScenarioContentView: UIViewControllerRepresentable {
- var kind: ScenarioKind
- var scenario: Scenario
- var additionalSafeAreaInsets: UIEdgeInsets
-
- @WeakReference
- private var contentUIView: UIView?
-
- init(
- kind: ScenarioKind,
- scenario: Scenario,
- additionalSafeAreaInsets: UIEdgeInsets = .zero,
- contentUIView: WeakReference
- ) {
- self.kind = kind
- self.scenario = scenario
- self.additionalSafeAreaInsets = additionalSafeAreaInsets
- self._contentUIView = contentUIView
- }
-
- init(
- kind: ScenarioKind,
- scenario: Scenario,
- additionalSafeAreaInsets: UIEdgeInsets = .zero
- ) {
- self.kind = kind
- self.scenario = scenario
- self.additionalSafeAreaInsets = additionalSafeAreaInsets
- }
-
- func makeUIViewController(context: Context) -> ScenarioViewController {
- let context = ScenarioContext(
- snapshotWaiter: SnapshotWaiter(),
- isSnapshot: false,
- screenSize: UIScreen.main.bounds.size
- )
- return ScenarioViewController(context: context)
- }
-
- func updateUIViewController(_ uiViewController: ScenarioViewController, context: Context) {
- contentUIView = uiViewController.view
- uiViewController.scenario = scenario
- uiViewController.additionalSafeAreaInsets = additionalSafeAreaInsets
- }
-}
diff --git a/Sources/PlaybookUI/Internal/ScenarioDisplay.swift b/Sources/PlaybookUI/Internal/ScenarioDisplay.swift
deleted file mode 100644
index a5038e9..0000000
--- a/Sources/PlaybookUI/Internal/ScenarioDisplay.swift
+++ /dev/null
@@ -1,130 +0,0 @@
-import SwiftUI
-
-internal struct ScenarioDisplay: View {
- enum Status {
- case `default`
- case waitForSnapshot
- case loaded(image: UIImage)
- case failed
-
- var isWaitForSnapshot: Bool {
- guard case .waitForSnapshot = self else { return false }
- return true
- }
-
- var isLoaded: Bool {
- guard case .loaded = self else { return false }
- return true
- }
- }
-
- static let scale: CGFloat = 0.3
-
- private var store: ScenarioDisplayStore
-
- @State
- private var status = Status.default
-
- @Environment(\.galleryDependency)
- private var dependency
-
- init(store: ScenarioDisplayStore) {
- self.store = store
- }
-
- var body: some View {
- VStack(spacing: 16) {
- ZStack {
- Color(.scenarioBackground)
-
- content()
- .id(AnimationID.content)
- }
- .compositingGroup()
- .transition(.opacity)
- .animation(.spring(), value: status.isWaitForSnapshot)
- .frame(
- width: contentWidth,
- height: store.snapshotLoader.device.size.height * Self.scale,
- alignment: .topLeading
- )
- .cornerRadius(12)
- .shadow(color: Color(.black).opacity(0.25), radius: shadowRadius)
-
- Text(store.data.scenario.name.rawValue)
- .font(.subheadline)
- .lineLimit(nil)
- .fixedSize(horizontal: false, vertical: true)
- .background(Highlight(store.data.shouldHighlight))
- }
- .frame(width: contentWidth + 16)
- .padding(shadowRadius * 2)
- .onVisibilityChanged(
- in: CGRect(origin: .zero, size: dependency.context.screenSize),
- perform: store.isVisible.send
- )
- .onReceive(store.isVisible.removeDuplicates(by: ==)) { isVisible in
- if isVisible {
- self.store.loadImage(into: self.$status)
- }
- else {
- self.store.cancellAll()
- }
- }
- }
-}
-
-private extension ScenarioDisplay {
- enum AnimationID {
- case content
- }
-
- var shadowRadius: CGFloat { 4 }
-
- var contentWidth: CGFloat {
- store.snapshotLoader.device.size.width * Self.scale
- }
-
- func content() -> some View {
- switch status {
- case .default, .waitForSnapshot:
- return AnyView(EmptyView())
-
- case .loaded(let image):
- return AnyView(
- Image(uiImage: image)
- .resizable()
- .frame(
- width: image.size.width * Self.scale,
- height: image.size.height * Self.scale
- )
- )
-
- case .failed:
- return AnyView(
- Text("Could not load snapshot image")
- .font(.caption)
- .bold()
- .lineLimit(nil)
- .padding(.horizontal, 8)
- )
- }
- }
-}
-
-private extension View {
- func onVisibilityChanged(in bounds: CGRect, perform: @escaping (Bool) -> Void) -> some View {
- background(
- GeometryReader { geometry -> Color in
- let vertical = geometry.size.height / 2
- let insets = UIEdgeInsets.only(top: vertical, bottom: vertical)
- let isVisible =
- bounds
- .inset(by: insets)
- .intersects(geometry.frame(in: .global))
- perform(isVisible)
- return Color.clear
- }
- )
- }
-}
diff --git a/Sources/PlaybookUI/Internal/ScenarioDisplayList.swift b/Sources/PlaybookUI/Internal/ScenarioDisplayList.swift
deleted file mode 100644
index d5ba812..0000000
--- a/Sources/PlaybookUI/Internal/ScenarioDisplayList.swift
+++ /dev/null
@@ -1,85 +0,0 @@
-import SwiftUI
-
-internal struct ScenarioDisplayList: View {
- var data: SearchedListData
- var safeAreaInsets: EdgeInsets
- var onSelect: (SearchedData) -> Void
-
- @EnvironmentObject
- private var store: GalleryStore
-
- @Environment(\.galleryDependency)
- private var dependency
-
- private var serialDispatcher: SerialMainDispatcher
-
- init(
- data: SearchedListData,
- safeAreaInsets: EdgeInsets,
- serialDispatcher: SerialMainDispatcher,
- onSelect: @escaping (SearchedData) -> Void
- ) {
- self.data = data
- self.safeAreaInsets = safeAreaInsets
- self.serialDispatcher = serialDispatcher
- self.onSelect = onSelect
- }
-
- var body: some View {
- VStack(alignment: .leading, spacing: 8) {
- HStack(spacing: 8) {
- Image(symbol: .bookmarkFill)
- .imageScale(.medium)
- .font(.system(size: 20))
- .foregroundColor(Color(.primaryBlue))
-
- Text(data.kind.rawValue)
- .foregroundColor(Color(.label))
- .font(.system(size: 24))
- .bold()
- .background(Highlight(data.shouldHighlight))
-
- Spacer.zero
- }
- .padding(.horizontal, 24)
- .padding(.leading, safeAreaInsets.leading)
- .padding(.trailing, safeAreaInsets.trailing)
-
- ScrollView(.horizontal, showsIndicators: false) {
- HStack(alignment: .top, spacing: 0) {
- ForEach(data.scenarios) { data in
- Button(
- action: { self.onSelect(data) },
- label: {
- ScenarioDisplay(
- store: ScenarioDisplayStore(
- data: data,
- snapshotLoader: self.store.snapshotLoader,
- serialDispatcher: self.serialDispatcher,
- scheduler: self.dependency.scheduler
- )
- )
- }
- )
- .buttonStyle(ScaleButtonStyle())
- }
- }
- .padding(.horizontal, 24)
- .padding(.leading, safeAreaInsets.leading)
- .padding(.trailing, safeAreaInsets.trailing)
- }
-
- HorizontalSeparator()
- }
- .padding(.vertical, 8)
- .onDisappear(perform: serialDispatcher.cancel)
- }
-}
-
-private struct ScaleButtonStyle: ButtonStyle {
- func makeBody(configuration: Configuration) -> some View {
- configuration.label
- .scaleEffect(configuration.isPressed ? 0.95 : 1)
- .animation(.interactiveSpring(), value: configuration.isPressed)
- }
-}
diff --git a/Sources/PlaybookUI/Internal/ScenarioDisplaySheet.swift b/Sources/PlaybookUI/Internal/ScenarioDisplaySheet.swift
deleted file mode 100644
index d3a3373..0000000
--- a/Sources/PlaybookUI/Internal/ScenarioDisplaySheet.swift
+++ /dev/null
@@ -1,115 +0,0 @@
-import SwiftUI
-
-internal struct ScenarioDisplaySheet: View {
- var data: SearchedData
- var onClose: () -> Void
-
- @EnvironmentObject
- private var store: GalleryStore
-
- @WeakReference
- private var contentUIView: UIView?
-
- init(
- data: SearchedData,
- onClose: @escaping () -> Void
- ) {
- self.data = data
- self.onClose = onClose
- }
-
- var body: some View {
- ZStack {
- ScenarioContentView(
- kind: data.kind,
- scenario: data.scenario,
- additionalSafeAreaInsets: .only(top: topBarHeight),
- contentUIView: _contentUIView
- )
- .edgesIgnoringSafeArea(.all)
- .background(
- Color(.scenarioBackground)
- .edgesIgnoringSafeArea(.all)
- )
-
- VStack(spacing: 0) {
- topBar()
-
- Divider()
- .edgesIgnoringSafeArea(.all)
-
- Spacer.zero
- }
- }
- .sheet(item: $store.shareItem) { item in
- ImageSharingView(item: item) { self.store.shareItem = nil }
- .edgesIgnoringSafeArea(.all)
- }
- .background(
- Color(.scenarioBackground)
- .edgesIgnoringSafeArea(.all)
- )
- }
-}
-
-private extension ScenarioDisplaySheet {
- var topBarHeight: CGFloat { 44 }
-
- func topBar() -> some View {
- HStack(spacing: 0) {
- shareButton()
-
- Spacer(minLength: 24)
-
- Text(data.scenario.name.rawValue)
- .bold()
- .lineLimit(1)
-
- Spacer(minLength: 24)
-
- closeButton()
- }
- .padding(.horizontal, 24)
- .frame(height: topBarHeight)
- .background(
- Blur(style: .systemMaterial)
- .edgesIgnoringSafeArea(.all),
- alignment: .topLeading
- )
- }
-
- func shareButton() -> some View {
- Button(action: share) {
- Image(symbol: .squareAndArrowUp)
- .imageScale(.large)
- .font(.headline)
- .foregroundColor(Color(.label))
- .frame(width: 30, height: 30)
- }
- }
-
- func closeButton() -> some View {
- Button(action: onClose) {
- ZStack {
- Color.gray.opacity(0.2)
- .clipShape(Circle())
- .frame(width: 30, height: 30)
-
- Image(symbol: .multiply)
- .imageScale(.large)
- .font(Font.subheadline.bold())
- .foregroundColor(.gray)
- }
- }
- }
-
- func share() {
- guard let uiView = contentUIView else { return }
-
- let image = UIGraphicsImageRenderer(bounds: uiView.bounds).image { _ in
- uiView.drawHierarchy(in: uiView.bounds, afterScreenUpdates: true)
- }
-
- store.shareItem = ImageSharingView.Item(image: image)
- }
-}
diff --git a/Sources/PlaybookUI/Internal/ScenarioDisplayStore.swift b/Sources/PlaybookUI/Internal/ScenarioDisplayStore.swift
deleted file mode 100644
index 2d43f65..0000000
--- a/Sources/PlaybookUI/Internal/ScenarioDisplayStore.swift
+++ /dev/null
@@ -1,108 +0,0 @@
-import Combine
-import SwiftUI
-
-internal final class ScenarioDisplayStore {
- let data: SearchedData
- let snapshotLoader: SnapshotLoaderProtocol
- let serialDispatcher: SerialMainDispatcher
- let isVisible = CurrentValueSubject(false)
-
- private let scheduler: SchedulerProtocol
-
- @Atomic
- private var cancellables = Set()
-
- init(
- data: SearchedData,
- snapshotLoader: SnapshotLoaderProtocol,
- serialDispatcher: SerialMainDispatcher,
- scheduler: SchedulerProtocol = Scheduler()
- ) {
- self.data = data
- self.snapshotLoader = snapshotLoader
- self.serialDispatcher = serialDispatcher
- self.scheduler = scheduler
- }
-
- func loadImage(into status: Binding) {
- guard !status.wrappedValue.isLoaded else { return }
-
- asyncLoadSnapshot()
- .flatMap { [weak self] status -> AnyPublisher in
- switch status {
- case .loaded, .failed:
- return Just(status)
- .eraseToAnyPublisher()
-
- case .default, .waitForSnapshot:
- guard let asyncTakeSnapshot = self?.asyncTakeSnapshot else {
- return Empty().eraseToAnyPublisher()
- }
-
- return Deferred(createPublisher: asyncTakeSnapshot)
- .merge(with: Just(status))
- .eraseToAnyPublisher()
- }
- }
- .assign(to: \.wrappedValue, on: status)
- .store(in: &cancellables)
- }
-
- func cancellAll() {
- _cancellables.modify { $0.removeAll() }
- }
-}
-
-private extension ScenarioDisplayStore {
- func asyncLoadSnapshot() -> Future {
- func run() -> ScenarioDisplay.Status {
- let result = snapshotLoader.loadImage(kind: data.kind, name: data.scenario.name)
-
- switch result {
- case .success(let image?):
- return .loaded(image: image)
-
- case .success(.none):
- return .waitForSnapshot
-
- case .failure:
- return .failed
- }
- }
-
- let scheduler = self.scheduler
-
- return Future { fulfill in
- scheduler.schedule(on: .imageLoadQueue) {
- fulfill(.success(run()))
- }
- }
- }
-
- func asyncTakeSnapshot() -> Future {
- Future { [weak self] fulfill in
- guard let self = self else {
- return fulfill(.success(.default))
- }
-
- self._cancellables.modify { cancellables in
- let cancellable = self.serialDispatcher.dispatch {
- self.snapshotLoader.takeSnapshot(for: self.data.scenario, kind: self.data.kind) { data in
- let status: ScenarioDisplay.Status = UIImage(data: data).map { .loaded(image: $0) } ?? .failed
- fulfill(.success(status))
- }
- }
- cancellables.insert(cancellable)
- }
- }
- }
-}
-
-private extension DispatchQueue {
- static let imageLoadQueue = DispatchQueue(
- label: "app.playbook-ui.ScenarioDisplay.imageLoadQueue",
- qos: .userInitiated,
- attributes: .concurrent,
- autoreleaseFrequency: .workItem
- )
-}
diff --git a/Sources/PlaybookUI/Internal/ScenarioSearchStore.swift b/Sources/PlaybookUI/Internal/ScenarioSearchStore.swift
deleted file mode 100644
index 7c89231..0000000
--- a/Sources/PlaybookUI/Internal/ScenarioSearchStore.swift
+++ /dev/null
@@ -1,90 +0,0 @@
-import SwiftUI
-
-internal class ScenarioSearchStore: ObservableObject {
- @Published
- private(set) var result = SearchResult(matchedCount: 0, data: [])
-
- let playbook: Playbook
-
- private(set) lazy var scenariosCount = playbook.stores.reduce(0) { $0 + $1.scenarios.count }
-
- var searchText: String? {
- didSet { search(searchText) }
- }
-
- init(playbook: Playbook) {
- self.playbook = playbook
- }
-
- @discardableResult
- func start(with searchText: String? = nil) -> Self {
- self.searchText = searchText
- return self
- }
-}
-
-private extension ScenarioSearchStore {
- func search(_ query: String?) {
- let query = query?.lowercased() ?? ""
-
- func isMatched(_ string: String) -> Bool {
- string.lowercased().contains(query)
- }
-
- let data: [SearchedListData]
-
- if query.isEmpty {
- data = playbook.stores.map { store in
- SearchedListData(
- kind: store.kind,
- shouldHighlight: false,
- scenarios: store.scenarios.map { scenario in
- SearchedData(
- scenario: scenario,
- kind: store.kind,
- shouldHighlight: false
- )
- }
- )
- }
- }
- else {
- data = playbook.stores.compactMap { store in
- if isMatched(store.kind.rawValue) {
- return SearchedListData(
- kind: store.kind,
- shouldHighlight: true,
- scenarios: store.scenarios.map { scenario in
- SearchedData(
- scenario: scenario,
- kind: store.kind,
- shouldHighlight: isMatched(scenario.name.rawValue)
- )
- }
- )
- }
- else {
- let data = SearchedListData(
- kind: store.kind,
- shouldHighlight: false,
- scenarios: store.scenarios.compactMap { scenario in
- guard isMatched(scenario.name.rawValue) else { return nil }
-
- return SearchedData(
- scenario: scenario,
- kind: store.kind,
- shouldHighlight: true
- )
- }
- )
- return data.scenarios.isEmpty ? nil : data
- }
- }
- }
-
- return result = SearchResult(
- matchedCount: data.reduce(0) { $0 + $1.scenarios.count },
- data: data
- )
- }
-}
diff --git a/Sources/PlaybookUI/Internal/ScenarioSearchTree.swift b/Sources/PlaybookUI/Internal/ScenarioSearchTree.swift
deleted file mode 100644
index cbfce89..0000000
--- a/Sources/PlaybookUI/Internal/ScenarioSearchTree.swift
+++ /dev/null
@@ -1,406 +0,0 @@
-import SwiftUI
-
-internal struct ScenarioSearchTree: View {
- @ViewBuilder
- var body: some View {
- #if swift(>=5.3)
- if #available(iOS 14.0, *) {
- ScenarioSearchTreeIOS14()
- }
- else {
- ScenarioSearchTreeIOS13()
- }
- #else
- ScenarioSearchTreeIOS13()
- #endif
- }
-}
-
-#if swift(>=5.3)
-@available(iOS 14.0, *)
-internal struct ScenarioSearchTreeIOS14: View {
- @EnvironmentObject
- var store: CatalogStore
-
- var body: some View {
- VStack(spacing: .zero) {
- searchBar()
-
- if store.result.data.isEmpty {
- emptyContent()
- }
- else {
- ScrollView {
- LazyVStack(spacing: .zero) {
- ForEach(store.result.data, id: \.kind) { data in
- let isOpened = currentOpenedKindsBinding().wrappedValue.contains(data.kind)
-
- kindRow(
- data: data,
- isOpened: isOpened
- )
-
- if isOpened {
- ForEach(data.scenarios, id: \.id) { data in
- scenarioRow(
- data: data,
- isSelected: data.id == store.selectedScenario?.id
- )
- }
- }
- }
- }
- }
- }
- }
- .background(
- Color(.secondaryBackground).ignoresSafeArea()
- )
- }
-}
-
-@available(iOS 14.0, *)
-private extension ScenarioSearchTreeIOS14 {
- func searchTextBinding() -> Binding {
- Binding(
- get: { store.searchText },
- set: { newValue in
- let isEmpty = newValue.map { $0.isEmpty } ?? true
- store.openedSearchingKinds = isEmpty ? nil : Set(store.result.data.map { $0.kind })
- store.searchText = newValue
- }
- )
- }
-
- func currentOpenedKindsBinding() -> Binding> {
- Binding($store.openedSearchingKinds) ?? $store.openedKinds
- }
-
- func kindRow(data: SearchedListData, isOpened: Bool) -> some View {
- Button(
- action: {
- if isOpened {
- currentOpenedKindsBinding().wrappedValue.remove(data.kind)
- }
- else {
- currentOpenedKindsBinding().wrappedValue.insert(data.kind)
- }
- },
- label: {
- VStack(spacing: .zero) {
- HStack(spacing: 8) {
- Image(symbol: .chevronRight)
- .imageScale(.small)
- .foregroundColor(Color(.label))
- .rotationEffect(.radians(isOpened ? .pi / 2 : 0))
-
- Image(symbol: .bookmarkFill)
- .imageScale(.medium)
- .foregroundColor(Color(.primaryBlue))
-
- Text(data.kind.rawValue)
- .bold()
- .font(.system(size: 20))
- .lineSpacing(4)
- .lineLimit(nil)
- .fixedSize(horizontal: false, vertical: true)
- .foregroundColor(Color(.label))
- .background(Highlight(data.shouldHighlight))
-
- Spacer(minLength: 16)
- }
- .padding(.vertical, 24)
-
- HorizontalSeparator()
- }
- .padding(.leading, 16)
- }
- )
- }
-
- func scenarioRow(data: SearchedData, isSelected: Bool) -> some View {
- Button(
- action: {
- store.selectedScenario = data
- },
- label: {
- VStack(spacing: .zero) {
- HStack(spacing: 8) {
- Image(symbol: .circleFill)
- .font(.system(size: 10))
- .foregroundColor(Color(isSelected ? .primaryBlue : .tertiarySystemFill))
-
- Text(data.scenario.name.rawValue)
- .font(.subheadline)
- .bold()
- .lineLimit(nil)
- .lineSpacing(4)
- .fixedSize(horizontal: false, vertical: true)
- .foregroundColor(Color(.label))
- .background(Highlight(data.shouldHighlight))
-
- Spacer(minLength: 16)
- }
- .padding(.vertical, 16)
-
- HorizontalSeparator()
- }
- .padding(.leading, 56)
- }
- )
- }
-
- func emptyContent() -> some View {
- VStack(spacing: .zero) {
- Text("This filter resulted in 0 results")
- .foregroundColor(Color(.label))
- .font(.body)
- .bold()
- .lineLimit(nil)
- .padding(24)
- .padding(.top, 24)
-
- Spacer.zero
- }
- }
-
- func searchBar() -> some View {
- VStack(spacing: .zero) {
- SearchBar(text: searchTextBinding(), height: 44)
-
- Counter(
- numerator: store.result.matchedCount,
- denominator: store.scenariosCount
- )
-
- HorizontalSeparator()
- .padding(.top, 8)
- }
- }
-}
-#endif
-
-internal struct ScenarioSearchTreeIOS13: View {
- @EnvironmentObject
- private var store: CatalogStore
-
- var body: some View {
- VStack(spacing: 0) {
- searchBar()
-
- if store.result.data.isEmpty {
- emptyContent()
- }
- else {
- TableView(
- snapshot: snapshot(),
- configureUIView: configureTableView,
- row: row,
- onSelect: onSelect
- )
- .edgesIgnoringSafeArea(.bottom)
- }
- }
- .background(
- Color(.secondaryBackground)
- .edgesIgnoringSafeArea(.all)
- )
- }
-}
-
-private extension ScenarioSearchTreeIOS13 {
- struct Section: Hashable {
- var data: SearchedListData
-
- func hash(into hasher: inout Hasher) {
- hasher.combine(data.kind)
- }
-
- static func == (lhs: Section, rhs: Section) -> Bool {
- lhs.data.kind == rhs.data.kind && lhs.data.shouldHighlight == rhs.data.shouldHighlight
- }
- }
-
- enum Row: Hashable {
- case kind(data: SearchedListData, isOpened: Bool)
- case scenario(data: SearchedData, isSelected: Bool)
-
- func hash(into hasher: inout Hasher) {
- switch self {
- case .kind(let data, isOpened: _):
- hasher.combine(data.kind)
-
- case .scenario(let data, isSelected: _):
- hasher.combine(data.id)
- }
- }
-
- static func == (lhs: Row, rhs: Row) -> Bool {
- switch (lhs, rhs) {
- case (.kind(let lData, let lIsOpened), .kind(let rData, let rIsOpened)):
- return lData.kind == rData.kind
- && lData.shouldHighlight == rData.shouldHighlight
- && lIsOpened == rIsOpened
-
- case (.scenario(let lData, let lIsSelected), .scenario(let rData, let rIsSelected)):
- return lData.kind == rData.kind
- && lData.scenario.name == rData.scenario.name
- && lData.shouldHighlight == rData.shouldHighlight
- && lIsSelected == rIsSelected
-
- default:
- return false
- }
- }
- }
-
- func currentOpenedKindsBinding() -> Binding> {
- Binding($store.openedSearchingKinds) ?? $store.openedKinds
- }
-
- func searchTextBinding() -> Binding {
- Binding(
- get: { self.store.searchText },
- set: { newValue in
- let isEmpty = newValue.map { $0.isEmpty } ?? true
- self.store.openedSearchingKinds = isEmpty ? nil : Set(self.store.result.data.map { $0.kind })
- self.store.searchText = newValue
- }
- )
- }
-
- func searchBar() -> some View {
- VStack(spacing: .zero) {
- SearchBar(text: searchTextBinding(), height: 44)
-
- Counter(
- numerator: store.result.matchedCount,
- denominator: store.scenariosCount
- )
-
- HorizontalSeparator()
- .padding(.top, 8)
- }
- }
-
- func row(with row: Row) -> some View {
- switch row {
- case .kind(let data, let isOpened):
- return AnyView(kindRow(data: data, isOpened: isOpened))
-
- case .scenario(let data, let isSelected):
- return AnyView(scenarioRow(data: data, isSelected: isSelected))
- }
- }
-
- func kindRow(data: SearchedListData, isOpened: Bool) -> some View {
- VStack(spacing: 0) {
- HStack(spacing: 8) {
- Image(symbol: .chevronRight)
- .imageScale(.small)
- .foregroundColor(Color(.label))
- .rotationEffect(.radians(isOpened ? .pi / 2 : 0))
-
- Image(symbol: .bookmarkFill)
- .imageScale(.medium)
- .foregroundColor(Color(.primaryBlue))
-
- Text(data.kind.rawValue)
- .bold()
- .font(.system(size: 20))
- .lineSpacing(4)
- .lineLimit(nil)
- .fixedSize(horizontal: false, vertical: true)
- .foregroundColor(Color(.label))
- .background(Highlight(data.shouldHighlight))
-
- Spacer(minLength: 16)
- }
- .padding(.vertical, 24)
-
- HorizontalSeparator()
- }
- .padding(.leading, 16)
- }
-
- func scenarioRow(data: SearchedData, isSelected: Bool) -> some View {
- VStack(spacing: 0) {
- HStack(spacing: 8) {
- Image(symbol: .circleFill)
- .font(.system(size: 10))
- .foregroundColor(Color(isSelected ? .primaryBlue : .tertiarySystemFill))
-
- Text(data.scenario.name.rawValue)
- .font(.subheadline)
- .bold()
- .lineLimit(nil)
- .lineSpacing(4)
- .fixedSize(horizontal: false, vertical: true)
- .foregroundColor(Color(.label))
- .background(Highlight(data.shouldHighlight))
-
- Spacer(minLength: 16)
- }
- .padding(.vertical, 16)
-
- HorizontalSeparator()
- }
- .padding(.leading, 56)
- }
-
- func emptyContent() -> some View {
- VStack(spacing: 0) {
- Text("This filter resulted in 0 results")
- .foregroundColor(Color(.label))
- .font(.body)
- .bold()
- .lineLimit(nil)
- .padding(24)
-
- Spacer.zero
- }
- .padding(.top, 24)
- }
-
- func snapshot() -> NSDiffableDataSourceSnapshot {
- var snapshot = NSDiffableDataSourceSnapshot()
-
- guard !store.result.data.isEmpty else {
- return snapshot
- }
-
- let sections = store.result.data.map(Section.init)
- snapshot.appendSections(sections)
-
- for section in sections {
- let isOpened = currentOpenedKindsBinding().wrappedValue.contains(section.data.kind)
- let scenarios = isOpened ? section.data.scenarios.map { Row.scenario(data: $0, isSelected: $0.id == store.selectedScenario?.id) } : []
- snapshot.appendItems([.kind(data: section.data, isOpened: isOpened)] + scenarios, toSection: section)
- }
-
- return snapshot
- }
-
- func onSelect(_ row: Row) {
- switch row {
- case .kind(let data, let isOpened):
- if isOpened {
- currentOpenedKindsBinding().wrappedValue.remove(data.kind)
- }
- else {
- currentOpenedKindsBinding().wrappedValue.insert(data.kind)
- }
-
- case .scenario(let data, isSelected: _):
- store.selectedScenario = data
- }
- }
-
- func configureTableView(_ tableView: UITableView) {
- tableView.estimatedRowHeight = 70
- tableView.contentInset.bottom = 44
- tableView.separatorStyle = .none
- tableView.keyboardDismissMode = .onDrag
- tableView.insetsContentViewsToSafeArea = false
- }
-}
diff --git a/Sources/PlaybookUI/Internal/Scheduler.swift b/Sources/PlaybookUI/Internal/Scheduler.swift
deleted file mode 100644
index 43df243..0000000
--- a/Sources/PlaybookUI/Internal/Scheduler.swift
+++ /dev/null
@@ -1,16 +0,0 @@
-import SwiftUI
-
-internal protocol SchedulerProtocol {
- func schedule(on: DispatchQueue, action: @escaping () -> Void)
- func schedule(on: DispatchQueue, after interval: TimeInterval, action: @escaping () -> Void)
-}
-
-internal struct Scheduler: SchedulerProtocol {
- func schedule(on queue: DispatchQueue, action: @escaping () -> Void) {
- queue.async(execute: action)
- }
-
- func schedule(on queue: DispatchQueue, after interval: TimeInterval = 0, action: @escaping () -> Void) {
- queue.asyncAfter(deadline: .now() + interval, execute: action)
- }
-}
diff --git a/Sources/PlaybookUI/Internal/SearchBar.swift b/Sources/PlaybookUI/Internal/SearchBar.swift
deleted file mode 100644
index 1cb9b3a..0000000
--- a/Sources/PlaybookUI/Internal/SearchBar.swift
+++ /dev/null
@@ -1,78 +0,0 @@
-import SwiftUI
-import UIKit
-
-internal struct SearchBar: View {
- @Binding
- var text: String?
- var height: CGFloat
-
- var body: some View {
- SearchBarWrapper(text: $text, placeholder: "Search") { searchBar in
- let backgroundImage = UIColor.tertiarySystemFill.circleImage(length: self.height)
- searchBar.setSearchFieldBackgroundImage(backgroundImage, for: .normal)
- }
- .animation(nil)
- .accentColor(Color(.primaryBlue))
- .frame(height: height)
- .padding(.top, 16)
- .padding(.horizontal, 8)
- }
-}
-
-private struct SearchBarWrapper: UIViewRepresentable {
- @Binding
- var text: String?
-
- var placeholder: String?
- var updateUIView: ((UISearchBar) -> Void)?
-
- func makeCoordinator() -> Coordinator {
- Coordinator(self)
- }
-
- func makeUIView(context: Context) -> UISearchBar {
- let searchBar = UISearchBar()
- searchBar.enablesReturnKeyAutomatically = false
- searchBar.backgroundImage = UIImage()
- searchBar.setPositionAdjustment(UIOffset(horizontal: 8, vertical: 0), for: .search)
- searchBar.setPositionAdjustment(UIOffset(horizontal: -8, vertical: 0), for: .clear)
- return searchBar
- }
-
- func updateUIView(_ uiView: UISearchBar, context: Context) {
- uiView.text = text
- uiView.placeholder = placeholder
- uiView.delegate = context.coordinator
- updateUIView?(uiView)
- }
-}
-
-private extension SearchBarWrapper {
- final class Coordinator: NSObject, UISearchBarDelegate {
- let base: SearchBarWrapper
-
- init(_ base: SearchBarWrapper) {
- self.base = base
- }
-
- func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
- base.text = searchText
- }
-
- func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
- searchBar.setShowsCancelButton(true, animated: true)
- }
-
- func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
- searchBar.setShowsCancelButton(false, animated: true)
- }
-
- func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
- searchBar.resignFirstResponder()
- }
-
- func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
- searchBar.resignFirstResponder()
- }
- }
-}
diff --git a/Sources/PlaybookUI/Internal/SearchResult.swift b/Sources/PlaybookUI/Internal/SearchResult.swift
deleted file mode 100644
index 5971f9d..0000000
--- a/Sources/PlaybookUI/Internal/SearchResult.swift
+++ /dev/null
@@ -1,4 +0,0 @@
-internal struct SearchResult {
- var matchedCount: Int
- var data: [SearchedListData]
-}
diff --git a/Sources/PlaybookUI/Internal/SearchedData.swift b/Sources/PlaybookUI/Internal/SearchedData.swift
deleted file mode 100644
index 6cdadde..0000000
--- a/Sources/PlaybookUI/Internal/SearchedData.swift
+++ /dev/null
@@ -1,14 +0,0 @@
-internal struct SearchedData: Identifiable {
- struct ID: Hashable {
- var kind: ScenarioKind
- var name: ScenarioName
- }
-
- var scenario: Scenario
- var kind: ScenarioKind
- var shouldHighlight: Bool
-
- var id: ID {
- ID(kind: kind, name: scenario.name)
- }
-}
diff --git a/Sources/PlaybookUI/Internal/SearchedListData.swift b/Sources/PlaybookUI/Internal/SearchedListData.swift
deleted file mode 100644
index 810b40a..0000000
--- a/Sources/PlaybookUI/Internal/SearchedListData.swift
+++ /dev/null
@@ -1,5 +0,0 @@
-internal struct SearchedListData {
- var kind: ScenarioKind
- var shouldHighlight: Bool
- var scenarios: [SearchedData]
-}
diff --git a/Sources/PlaybookUI/Internal/SerialMainDispatcher.swift b/Sources/PlaybookUI/Internal/SerialMainDispatcher.swift
deleted file mode 100644
index 5125e0c..0000000
--- a/Sources/PlaybookUI/Internal/SerialMainDispatcher.swift
+++ /dev/null
@@ -1,80 +0,0 @@
-import Combine
-import Foundation
-
-internal final class SerialMainDispatcher {
- private struct Resource {
- var queue = ContiguousArray()
- var cancellable: AnyCancellable?
- }
-
- private let interval: TimeInterval
-
- @Atomic
- private var resource = Resource()
-
- private var scheduler: SchedulerProtocol
-
- init(interval: TimeInterval, scheduler: SchedulerProtocol) {
- self.interval = interval
- self.scheduler = scheduler
- }
-
- func dispatch(block: @escaping () -> Void) -> AnyCancellable {
- let workItem = DispatchWorkItem(block: block)
- let shouldStart: Bool = _resource.modify { resource in
- let isEmpty = resource.queue.isEmpty
- resource.queue.append(workItem)
- return isEmpty
- }
-
- if shouldStart {
- executeNext()
- }
-
- return AnyCancellable(workItem.cancel)
- }
-
- func cancel() {
- resource = Resource()
- }
-}
-
-private extension SerialMainDispatcher {
- func executeNext() {
- let nextWorkItem: DispatchWorkItem? = _resource.modify { resource in
- guard let workItem = resource.queue.first else { return nil }
-
- if workItem.isCancelled {
- resource.queue.removeFirst()
- return workItem
- }
- else {
- resource.cancellable = AnyCancellable(workItem.cancel)
- return workItem
- }
- }
-
- guard let workItem = nextWorkItem else {
- return
- }
-
- guard !workItem.isCancelled else {
- return executeNext()
- }
-
- scheduler.schedule(on: .main, after: interval) { [weak self] in
- guard let self = self else { return }
-
- if !workItem.isCancelled {
- workItem.perform()
- }
-
- self._resource.modify {
- guard !$0.queue.isEmpty else { return }
- $0.queue.removeFirst()
- }
-
- self.executeNext()
- }
- }
-}
diff --git a/Sources/PlaybookUI/Internal/SnapshotLoader.swift b/Sources/PlaybookUI/Internal/SnapshotLoader.swift
deleted file mode 100644
index 81a5681..0000000
--- a/Sources/PlaybookUI/Internal/SnapshotLoader.swift
+++ /dev/null
@@ -1,86 +0,0 @@
-import SwiftUI
-
-internal protocol SnapshotLoaderProtocol {
- var device: SnapshotDevice { get }
-
- func takeSnapshot(for scenario: Scenario, kind: ScenarioKind, completion: ((Data) -> Void)?)
- func loadImage(kind: ScenarioKind, name: ScenarioName) -> Result
- func clean()
-}
-
-internal final class SnapshotLoader: SnapshotLoaderProtocol {
- let name: String
- let baseDirectoryURL: URL
- let format: SnapshotSupport.ImageFormat
- let device: SnapshotDevice
-
- var directoryURL: URL {
- baseDirectoryURL.appendingPathComponent(name, isDirectory: true)
- }
-
- init(
- name: String,
- baseDirectoryURL: URL,
- format: SnapshotSupport.ImageFormat,
- device: SnapshotDevice
- ) {
- self.baseDirectoryURL = baseDirectoryURL
- self.name = name
- self.format = format
- self.device = device
- }
-
- func takeSnapshot(for scenario: Scenario, kind: ScenarioKind, completion: ((Data) -> Void)?) {
- SnapshotSupport.data(
- for: scenario,
- on: device,
- format: format,
- scale: 1,
- handler: { [weak self] data in
- self?.storeImage(data: data, kind: kind, name: scenario.name)
- completion?(data)
- }
- )
- }
-
- func loadImage(kind: ScenarioKind, name: ScenarioName) -> Result {
- Result {
- let fileURL = self.fileURL(kind: kind, name: name)
-
- if FileManager.default.fileExists(atPath: fileURL.path) {
- let data = try Data(contentsOf: fileURL)
- return UIImage(data: data)
- }
- else {
- return nil
- }
- }
- }
-
- func clean() {
- if FileManager.default.fileExists(atPath: directoryURL.path) {
- try? FileManager.default.removeItem(at: directoryURL)
- }
- }
-}
-
-private extension SnapshotLoader {
- func fileURL(kind: ScenarioKind, name: ScenarioName) -> URL {
- directoryURL
- .appendingPathComponent(kind.rawValue, isDirectory: true)
- .appendingPathComponent(name.rawValue)
- .appendingPathExtension(format.fileExtension)
- }
-
- func storeImage(data: Data, kind: ScenarioKind, name: ScenarioName) {
- let fileManager = FileManager.default
- let fileURL = self.fileURL(kind: kind, name: name)
- let kindDirectoryURL = fileURL.deletingLastPathComponent()
-
- if !fileManager.fileExists(atPath: kindDirectoryURL.path) {
- try? fileManager.createDirectory(at: kindDirectoryURL, withIntermediateDirectories: true)
- }
-
- try? data.write(to: fileURL)
- }
-}
diff --git a/Sources/PlaybookUI/Internal/State/CatalogState.swift b/Sources/PlaybookUI/Internal/State/CatalogState.swift
new file mode 100644
index 0000000..b8f06fc
--- /dev/null
+++ b/Sources/PlaybookUI/Internal/State/CatalogState.swift
@@ -0,0 +1,49 @@
+import Playbook
+import SwiftUI
+
+@MainActor
+internal final class CatalogState: ObservableObject {
+ @Published
+ var selected: SelectData?
+ @Published
+ var isSearchPainCollapsed = true
+ @Published
+ var colorScheme: ColorScheme?
+ @Published
+ var expandedKinds = Set()
+ @Published
+ var searchingKinds: Set?
+
+ var currentExpandedKinds: Set {
+ get { searchingKinds ?? expandedKinds }
+ set {
+ if searchingKinds != nil {
+ searchingKinds = newValue
+ }
+ else {
+ expandedKinds = newValue
+ }
+ }
+ }
+
+ func selectInitial(searchResult: SearchResult) {
+ guard selected == nil else {
+ return
+ }
+
+ let scenario = searchResult.kinds.first?.scenarios.first
+ selected = scenario.map { scenario in
+ SelectData(
+ kind: scenario.kind,
+ scenario: scenario.scenario
+ )
+ }
+ }
+
+ func updateSearchingKinds(query: String, searchResult: SearchResult) {
+ searchingKinds =
+ query.isEmpty
+ ? nil
+ : Set(searchResult.kinds.map(\.kind))
+ }
+}
diff --git a/Sources/PlaybookUI/Internal/State/GalleryState.swift b/Sources/PlaybookUI/Internal/State/GalleryState.swift
new file mode 100644
index 0000000..79ed45a
--- /dev/null
+++ b/Sources/PlaybookUI/Internal/State/GalleryState.swift
@@ -0,0 +1,14 @@
+import SwiftUI
+
+@MainActor
+internal final class GalleryState: ObservableObject {
+ @Published
+ var selected: SelectData?
+ @Published
+ var colorScheme: ColorScheme?
+
+ func clearImageCache() {
+ let imageCache = ImageCache()
+ imageCache.clear()
+ }
+}
diff --git a/Sources/PlaybookUI/Internal/State/SearchState.swift b/Sources/PlaybookUI/Internal/State/SearchState.swift
new file mode 100644
index 0000000..f5d7b32
--- /dev/null
+++ b/Sources/PlaybookUI/Internal/State/SearchState.swift
@@ -0,0 +1,88 @@
+import Playbook
+import SwiftUI
+
+@MainActor
+internal final class SearchState: ObservableObject {
+ private let playbook: Playbook
+
+ @Published
+ var query = "" {
+ didSet { search(query: query) }
+ }
+
+ @Published
+ private(set) var result = SearchResult(count: 0, total: 0, kinds: [])
+
+ init(playbook: Playbook) {
+ self.playbook = playbook
+ search(query: query)
+ }
+}
+
+private extension SearchState {
+ func search(query: String) {
+ let query = query.lowercased()
+
+ func matchedRange(_ string: String) -> Range? {
+ string.lowercased().range(of: query)
+ }
+
+ let kinds: [SearchedKindData] =
+ if query.isEmpty {
+ playbook.stores.map { store in
+ SearchedKindData(
+ kind: store.kind,
+ highlightRange: nil,
+ scenarios: store.scenarios.map { scenario in
+ SearchedData(
+ kind: store.kind,
+ scenario: scenario,
+ highlightRange: nil
+ )
+ }
+ )
+ }
+ }
+ else {
+ playbook.stores.compactMap { store in
+ if let range = matchedRange(store.kind.rawValue) {
+ return SearchedKindData(
+ kind: store.kind,
+ highlightRange: range,
+ scenarios: store.scenarios.map { scenario in
+ SearchedData(
+ kind: store.kind,
+ scenario: scenario,
+ highlightRange: matchedRange(scenario.name.rawValue)
+ )
+ }
+ )
+ }
+ else {
+ let data = SearchedKindData(
+ kind: store.kind,
+ highlightRange: nil,
+ scenarios: store.scenarios.compactMap { scenario in
+ guard let range = matchedRange(scenario.name.rawValue) else {
+ return nil
+ }
+
+ return SearchedData(
+ kind: store.kind,
+ scenario: scenario,
+ highlightRange: range
+ )
+ }
+ )
+ return data.scenarios.isEmpty ? nil : data
+ }
+ }
+ }
+
+ result = SearchResult(
+ count: kinds.reduce(0) { $0 + $1.scenarios.count },
+ total: playbook.stores.reduce(0) { $0 + $1.scenarios.count },
+ kinds: kinds
+ )
+ }
+}
diff --git a/Sources/PlaybookUI/Internal/State/ShareState.swift b/Sources/PlaybookUI/Internal/State/ShareState.swift
new file mode 100644
index 0000000..18240b3
--- /dev/null
+++ b/Sources/PlaybookUI/Internal/State/ShareState.swift
@@ -0,0 +1,37 @@
+import Playbook
+import SwiftUI
+
+@MainActor
+internal final class ShareState: ObservableObject {
+ weak var scenarioViewController: ScenarioViewController?
+
+ func shareSnapshot() {
+ guard let scenarioViewController else {
+ return
+ }
+
+ let bounds = scenarioViewController.view.bounds
+ let image = UIGraphicsImageRenderer(bounds: bounds).image { _ in
+ scenarioViewController.view.drawHierarchy(in: bounds, afterScreenUpdates: true)
+ }
+ let activityViewController = UIActivityViewController(
+ activityItems: [image],
+ applicationActivities: nil
+ )
+
+ if !Bundle.main.hasPhotoLibraryAddUsageDescription {
+ activityViewController.excludedActivityTypes = [.saveToCameraRoll]
+ }
+
+ activityViewController.popoverPresentationController?.sourceView = scenarioViewController.view
+ activityViewController.popoverPresentationController?.permittedArrowDirections = []
+ scenarioViewController.present(activityViewController, animated: true)
+ }
+}
+
+private extension Bundle {
+ var hasPhotoLibraryAddUsageDescription: Bool {
+ let usage = object(forInfoDictionaryKey: "NSPhotoLibraryAddUsageDescription") as? String
+ return usage.map { !$0.isEmpty } ?? false
+ }
+}
diff --git a/Sources/PlaybookUI/Internal/TableView.swift b/Sources/PlaybookUI/Internal/TableView.swift
deleted file mode 100644
index ef4148e..0000000
--- a/Sources/PlaybookUI/Internal/TableView.swift
+++ /dev/null
@@ -1,137 +0,0 @@
-import SwiftUI
-
-internal struct TableView: UIViewRepresentable {
- var animated: Bool
- var snapshot: NSDiffableDataSourceSnapshot
- var configureUIView: ((UITableView) -> Void)?
- var row: (RowData) -> Row
- var onSelect: ((RowData) -> Void)?
-
- init(
- animated: Bool = true,
- snapshot: NSDiffableDataSourceSnapshot,
- configureUIView: ((UITableView) -> Void)?,
- row: @escaping (RowData) -> Row,
- onSelect: ((RowData) -> Void)? = nil
- ) {
- self.animated = animated
- self.snapshot = snapshot
- self.configureUIView = configureUIView
- self.row = row
- self.onSelect = onSelect
- }
-
- func makeCoordinator() -> Coordinator {
- Coordinator(base: self)
- }
-
- func makeUIView(context: Context) -> UIViewType {
- let tableView = UIViewType()
- let dataSource = UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, _ in
- context.coordinator.dequeueCell(for: tableView, indexPath: indexPath)
- }
-
- tableView.backgroundColor = .clear
- dataSource.defaultRowAnimation = .fade
- context.coordinator.dataSource = dataSource
- configureUIView?(tableView)
-
- return tableView
- }
-
- func updateUIView(_ tableView: UIViewType, context: Context) {
- context.coordinator.base = self
- tableView.dataSource = context.coordinator.dataSource
- tableView.delegate = context.coordinator
- tableView.allowsSelection = onSelect != nil
- tableView.readyForUpdate = nil
-
- if tableView.window != nil {
- let dataSource = context.coordinator.dataSource
- let isEmptyBefore = dataSource?.snapshot().sectionIdentifiers.isEmpty ?? false
- dataSource?.apply(snapshot, animatingDifferences: animated && !isEmptyBefore)
- }
- else {
- tableView.readyForUpdate = { [weak tableView] in
- guard let tableView = tableView else { return }
- self.updateUIView(tableView, context: context)
- }
- }
- }
-}
-
-internal extension TableView {
- final class UIViewType: UITableView {
- var readyForUpdate: (() -> Void)?
-
- override func layoutSubviews() {
- if window != nil {
- super.layoutSubviews()
- readyForUpdate?()
- readyForUpdate = nil
- }
- }
- }
-
- final class Coordinator: NSObject, UITableViewDelegate {
- var base: TableView
- var dataSource: UITableViewDiffableDataSource?
-
- init(base: TableView) {
- self.base = base
- }
-
- func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
- guard let onSelect = base.onSelect else { return }
-
- let sectionData = base.snapshot.sectionIdentifiers[indexPath.section]
- let rowData = base.snapshot.itemIdentifiers(inSection: sectionData)[indexPath.row]
-
- onSelect(rowData)
- tableView.deselectRow(at: indexPath, animated: false)
- }
-
- func dequeueCell(for tableView: UITableView, indexPath: IndexPath) -> Cell {
- let sectionData = base.snapshot.sectionIdentifiers[indexPath.section]
- let rowData = base.snapshot.itemIdentifiers(inSection: sectionData)[indexPath.row]
- let reuseIdentifier = rowData.hashValue.description
-
- guard let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier) as? Cell else {
- tableView.register(Cell.self, forCellReuseIdentifier: reuseIdentifier)
- return dequeueCell(for: tableView, indexPath: indexPath)
- }
-
- cell.hostingController.rootView = base.row(rowData)
- return cell
- }
- }
-
- final class Cell: UITableViewCell {
- let hostingController = UIHostingController(rootView: nil)
-
- override init(style: CellStyle, reuseIdentifier: String?) {
- super.init(style: style, reuseIdentifier: reuseIdentifier)
- layout()
- }
-
- @available(*, unavailable)
- required init?(coder: NSCoder) {
- super.init(coder: coder)
- }
-
- private func layout() {
- hostingController.view.backgroundColor = .clear
- hostingController.view.translatesAutoresizingMaskIntoConstraints = false
- backgroundColor = .clear
- contentView.backgroundColor = .clear
- contentView.addSubview(hostingController.view)
-
- NSLayoutConstraint.activate([
- contentView.topAnchor.constraint(equalTo: hostingController.view.topAnchor),
- contentView.bottomAnchor.constraint(equalTo: hostingController.view.bottomAnchor),
- contentView.leadingAnchor.constraint(equalTo: hostingController.view.leadingAnchor),
- contentView.trailingAnchor.constraint(equalTo: hostingController.view.trailingAnchor),
- ])
- }
- }
-}
diff --git a/Sources/PlaybookUI/Internal/Utilities/Image.swift b/Sources/PlaybookUI/Internal/Utilities/Image.swift
new file mode 100644
index 0000000..d2fe20e
--- /dev/null
+++ b/Sources/PlaybookUI/Internal/Utilities/Image.swift
@@ -0,0 +1,21 @@
+import SwiftUI
+
+internal extension Image {
+ enum SFSymbols: String {
+ case xmark
+ case xmarkCircleFill = "xmark.circle.fill"
+ case ellipsisCircle = "ellipsis.circle"
+ case magnifyingglass
+ case sunMax = "sun.max"
+ case moon
+ case docTextMagnifyingglass = "doc.text.magnifyingglass"
+ case chevronRight = "chevron.right"
+ case circleFill = "circle.fill"
+ case sidebarLeft = "sidebar.left"
+ case photo
+ }
+
+ init(symbol: SFSymbols) {
+ self.init(systemName: symbol.rawValue)
+ }
+}
diff --git a/Sources/PlaybookUI/Internal/Utilities/ImageCache.swift b/Sources/PlaybookUI/Internal/Utilities/ImageCache.swift
new file mode 100644
index 0000000..f2943af
--- /dev/null
+++ b/Sources/PlaybookUI/Internal/Utilities/ImageCache.swift
@@ -0,0 +1,89 @@
+import Playbook
+import SwiftUI
+
+internal struct ImageCache {
+ private let fileManager = FileManager.default
+
+ func data(for source: ImageSource) -> Data? {
+ let url = fileURL(for: source)
+
+ do {
+ if fileManager.fileExists(atPath: url.path) {
+ return try Data(contentsOf: url)
+ }
+ else {
+ return nil
+ }
+ }
+ catch {
+ debugLog(error: error, description: "Failed to read cache data.")
+ remove(at: url)
+ return nil
+ }
+ }
+
+ func create(file: Data, for source: ImageSource) {
+ do {
+ let fileURL = fileURL(for: source)
+ let directoryURL = fileURL.deletingPathExtension()
+
+ if !fileManager.fileExists(atPath: directoryURL.path) {
+ try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true)
+ }
+
+ try file.write(to: fileURL)
+ }
+ catch {
+ debugLog(error: error, description: "Failed to create cache data.")
+ }
+ }
+
+ func clear() {
+ let url = baseDirectoryURL()
+ remove(at: url)
+ }
+}
+
+private extension ImageCache {
+ static let normalizationCharacters = CharacterSet(charactersIn: ".:/")
+ .union(.whitespacesAndNewlines)
+ .union(.illegalCharacters)
+ .union(.controlCharacters)
+
+ func baseDirectoryURL() -> URL {
+ fileManager
+ .urls(for: .cachesDirectory, in: .userDomainMask).first!
+ .appendingPathComponent("app.playbook-ui", isDirectory: true)
+ }
+
+ func fileURL(for source: ImageSource) -> URL {
+ baseDirectoryURL()
+ .appendingPathComponent("images", isDirectory: true)
+ .appendingPathComponent(source.size.width.description, isDirectory: true)
+ .appendingPathComponent(source.size.height.description, isDirectory: true)
+ .appendingPathComponent(source.scale.description, isDirectory: true)
+ .appendingPathComponent("\(source.colorScheme)", isDirectory: true)
+ .appendingPathComponent(normalize(source.kind.rawValue), isDirectory: true)
+ .appendingPathComponent(normalize(source.scenario.name.rawValue))
+ .appendingPathExtension(SnapshotSupport.ImageFormat.png.fileExtension)
+ }
+
+ func remove(at url: URL) {
+ do {
+ try fileManager.removeItem(at: url)
+ }
+ catch {
+ debugLog(error: error, description: "Failed to clear cache data.")
+ }
+ }
+
+ func normalize(_ string: String) -> String {
+ string.components(separatedBy: Self.normalizationCharacters).joined(separator: "_")
+ }
+
+ func debugLog(error: @autoclosure () -> Error, description: @autoclosure () -> String) {
+ #if DEBUG
+ print("[Playbook]", description(), error())
+ #endif
+ }
+}
diff --git a/Sources/PlaybookUI/Internal/Utilities/ImageLoader.swift b/Sources/PlaybookUI/Internal/Utilities/ImageLoader.swift
new file mode 100644
index 0000000..febe7ad
--- /dev/null
+++ b/Sources/PlaybookUI/Internal/Utilities/ImageLoader.swift
@@ -0,0 +1,135 @@
+import Playbook
+import SwiftUI
+
+@MainActor
+internal final class ImageLoader: ObservableObject {
+ private let imageCache = ImageCache()
+ private var queue = ContiguousArray()
+ private var sequentiallyProcessedCount = 0
+
+ func loadImage(for source: ImageSource) async -> UIImage? {
+ if let data = imageCache.data(for: source) {
+ return UIImage(data: data)
+ }
+ else {
+ let item = QueueItem(source: source)
+ queue.append(item)
+
+ return await withTaskCancellationHandler {
+ await withCheckedContinuation { continuation in
+ item.continuation = continuation
+
+ if queue.count == 1 {
+ start()
+ }
+ }
+ } onCancel: {
+ item.isCancelled = true
+ item.continuation?.resume(returning: nil)
+ item.continuation = nil
+ }
+ }
+ }
+}
+
+private extension ImageLoader {
+ final class QueueItem {
+ let source: ImageSource
+ var continuation: CheckedContinuation?
+ var isCancelled = false
+
+ init(source: ImageSource) {
+ self.source = source
+ }
+ }
+
+ func start() {
+ guard let item = queue.first else {
+ return sequentiallyProcessedCount = 0
+ }
+
+ guard !item.isCancelled else {
+ queue.removeFirst()
+ return startNext()
+ }
+
+ sequentiallyProcessedCount += 1
+
+ // Perform snapshotting in `default` mode to avoid blocking UI tracking events.
+ RunLoop.main.perform(inModes: [.default]) { [weak self] in
+ guard let self else {
+ return
+ }
+
+ guard !item.isCancelled else {
+ queue.removeFirst()
+ return startNext()
+ }
+
+ SnapshotSupport.data(
+ for: item.source.scenario,
+ on: SnapshotDevice(
+ name: item.source.scenario.name.rawValue,
+ size: item.source.size,
+ traitCollection: UITraitCollection(
+ traitsFrom: [
+ UITraitCollection(userInterfaceStyle: item.source.colorScheme.userInterfaceStyle),
+ UITraitCollection(horizontalSizeClass: .compact),
+ UITraitCollection(verticalSizeClass: .regular),
+ UITraitCollection(layoutDirection: .leftToRight),
+ UITraitCollection(preferredContentSizeCategory: .large),
+ ]
+ )
+ ),
+ format: .png,
+ scale: item.source.scale
+ ) { [weak self] data in
+ guard let self else {
+ return
+ }
+
+ let image = UIImage(data: data)
+ item.continuation?.resume(returning: image)
+ item.continuation = nil
+ imageCache.create(file: data, for: item.source)
+ queue.removeFirst()
+ startNext()
+ }
+ }
+ }
+
+ func startNext() {
+ guard !queue.isEmpty else {
+ return
+ }
+
+ if sequentiallyProcessedCount < 4 {
+ start()
+ }
+ else {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
+ guard let self else {
+ return
+ }
+
+ sequentiallyProcessedCount = 0
+ start()
+ }
+ }
+ }
+}
+
+private extension ColorScheme {
+ var userInterfaceStyle: UIUserInterfaceStyle {
+ switch self {
+ case .light:
+ return .light
+
+ case .dark:
+ return .dark
+
+ @unknown default:
+ return .unspecified
+ }
+ }
+}
diff --git a/Sources/PlaybookUI/Internal/Utilities/ImageSource.swift b/Sources/PlaybookUI/Internal/Utilities/ImageSource.swift
new file mode 100644
index 0000000..9263bf8
--- /dev/null
+++ b/Sources/PlaybookUI/Internal/Utilities/ImageSource.swift
@@ -0,0 +1,10 @@
+import Playbook
+import SwiftUI
+
+struct ImageSource {
+ let scenario: Scenario
+ let kind: ScenarioKind
+ let size: CGSize
+ let scale: CGFloat
+ let colorScheme: ColorScheme
+}
diff --git a/Sources/PlaybookUI/Internal/Utilities/Spacer.swift b/Sources/PlaybookUI/Internal/Utilities/Spacer.swift
new file mode 100644
index 0000000..dd3590d
--- /dev/null
+++ b/Sources/PlaybookUI/Internal/Utilities/Spacer.swift
@@ -0,0 +1,11 @@
+import SwiftUI
+
+internal extension Spacer {
+ static var zero: Spacer {
+ Spacer(minLength: 0)
+ }
+
+ static func fixed(length: CGFloat) -> some View {
+ Spacer(minLength: length).fixedSize()
+ }
+}
diff --git a/Sources/PlaybookUI/Internal/Utilities/UIColor.swift b/Sources/PlaybookUI/Internal/Utilities/UIColor.swift
new file mode 100644
index 0000000..3310ad8
--- /dev/null
+++ b/Sources/PlaybookUI/Internal/Utilities/UIColor.swift
@@ -0,0 +1,21 @@
+import UIKit
+
+internal extension UIColor {
+ static let primaryBlue = UIColor(hex: 0x048DFF)
+ static let highlight = UIColor.systemYellow
+ static let translucentFill = UIColor.secondarySystemFill
+ static let primaryBackground = UIColor { traitCollection in
+ traitCollection.userInterfaceStyle == .light ? .white : .black
+ }
+ static let secondaryBackground = UIColor.secondarySystemBackground
+}
+
+private extension UIColor {
+ convenience init(hex: Int) {
+ let red = CGFloat((hex & 0xFF0000) >> 16) / 255
+ let green = CGFloat((hex & 0x00FF00) >> 8) / 255
+ let blue = CGFloat((hex & 0x0000FF) >> 0) / 255
+
+ self.init(red: red, green: green, blue: blue, alpha: 1)
+ }
+}
diff --git a/Sources/PlaybookUI/Internal/Utilities/UIImage.swift b/Sources/PlaybookUI/Internal/Utilities/UIImage.swift
new file mode 100644
index 0000000..37a3b14
--- /dev/null
+++ b/Sources/PlaybookUI/Internal/Utilities/UIImage.swift
@@ -0,0 +1,123 @@
+import SwiftUI
+
+internal extension UIImage {
+ static func circle(size: CGFloat) -> UIImage {
+ let size = CGSize(width: size, height: size)
+ return UIGraphicsImageRenderer(size: size).image { context in
+ let rect = CGRect(origin: .zero, size: size)
+ UIColor.tertiarySystemFill.setFill()
+ context.cgContext.fillEllipse(in: rect)
+ }
+ }
+
+ // Drawn with SwiftDraw: https://github.com/swhitty/SwiftDraw
+ static let logoMark = {
+ let size = CGSize(width: 142.0, height: 103.0)
+ return UIGraphicsImageRenderer(size: size).image { ctx in
+ let ctx = ctx.cgContext
+ ctx.saveGState()
+
+ let path = CGMutablePath()
+ path.move(to: CGPoint(x: 141.03, y: 2.28))
+ path.addCurve(
+ to: CGPoint(x: 139.21, y: 0.15),
+ control1: CGPoint(x: 141.79, y: 1.21),
+ control2: CGPoint(x: 140.73, y: 0.15)
+ )
+ path.addLine(to: CGPoint(x: 88.2, y: 0.15))
+ path.addLine(to: CGPoint(x: 87.9, y: 0.15))
+ path.addCurve(
+ to: CGPoint(x: 85.47, y: 1.37),
+ control1: CGPoint(x: 86.84, y: 0.15),
+ control2: CGPoint(x: 85.92, y: 0.61)
+ )
+ path.addLine(to: CGPoint(x: 16.4, y: 100.95))
+ path.addCurve(
+ to: CGPoint(x: 16.24, y: 101.41),
+ control1: CGPoint(x: 16.24, y: 101.11),
+ control2: CGPoint(x: 16.24, y: 101.26)
+ )
+ path.addCurve(
+ to: CGPoint(x: 17.46, y: 102.32),
+ control1: CGPoint(x: 16.24, y: 101.86),
+ control2: CGPoint(x: 16.85, y: 102.32)
+ )
+ path.addLine(to: CGPoint(x: 17.76, y: 102.32))
+ path.addLine(to: CGPoint(x: 22.01, y: 102.32))
+ path.addLine(to: CGPoint(x: 138.75, y: 102.47))
+ path.addCurve(
+ to: CGPoint(x: 140.58, y: 100.35),
+ control1: CGPoint(x: 140.27, y: 102.47),
+ control2: CGPoint(x: 141.34, y: 101.26)
+ )
+ path.addLine(to: CGPoint(x: 107.48, y: 52.07))
+ path.addCurve(
+ to: CGPoint(x: 107.48, y: 50.7),
+ control1: CGPoint(x: 107.18, y: 51.62),
+ control2: CGPoint(x: 107.18, y: 51.16)
+ )
+ path.addLine(to: CGPoint(x: 141.03, y: 2.28))
+ path.closeSubpath()
+ ctx.addPath(path)
+ ctx.clip()
+ ctx.setAlpha(1)
+
+ let rgb = CGColorSpaceCreateDeviceRGB()
+ let color1 = CGColor(colorSpace: rgb, components: [0.004, 0.525, 1, 1])!
+ let color2 = CGColor(colorSpace: rgb, components: [0.376, 0.871, 0.996, 1])!
+ var locations: [CGFloat] = [0.0, 0.9387]
+ let gradient = CGGradient(
+ colorsSpace: rgb,
+ colors: [color1, color2] as CFArray,
+ locations: &locations
+ )!
+ ctx.drawLinearGradient(
+ gradient,
+ start: CGPoint(x: 16.39, y: 51.21),
+ end: CGPoint(x: 136.7, y: 51.21),
+ options: [.drawsAfterEndLocation, .drawsBeforeStartLocation]
+ )
+ ctx.restoreGState()
+ ctx.saveGState()
+
+ let path1 = CGMutablePath()
+ path1.move(to: CGPoint(x: 69.98, y: 0.15))
+ path1.addLine(to: CGPoint(x: 2.58, y: 0))
+ path1.addCurve(
+ to: CGPoint(x: 0.15, y: 1.82),
+ control1: CGPoint(x: 1.21, y: 0),
+ control2: CGPoint(x: 0.15, y: 0.76)
+ )
+ path1.addLine(to: CGPoint(x: 0, y: 100.5))
+ path1.addCurve(
+ to: CGPoint(x: 4.71, y: 101.26),
+ control1: CGPoint(x: 0, y: 102.32),
+ control2: CGPoint(x: 3.49, y: 102.93)
+ )
+ path1.addLine(to: CGPoint(x: 72.41, y: 2.73))
+ path1.addCurve(
+ to: CGPoint(x: 69.98, y: 0.15),
+ control1: CGPoint(x: 73.02, y: 1.52),
+ control2: CGPoint(x: 71.96, y: 0.15)
+ )
+ path1.closeSubpath()
+ ctx.addPath(path1)
+ ctx.clip()
+ ctx.setAlpha(1)
+
+ var locations1: [CGFloat] = [0.0619712, 0.9387]
+ let gradient1 = CGGradient(
+ colorsSpace: rgb,
+ colors: [color1, color2] as CFArray,
+ locations: &locations1
+ )!
+ ctx.drawLinearGradient(
+ gradient1,
+ start: CGPoint(x: -0.18, y: 51.1),
+ end: CGPoint(x: 72.53, y: 51.1),
+ options: [.drawsAfterEndLocation, .drawsBeforeStartLocation]
+ )
+ ctx.restoreGState()
+ }
+ }()
+}
diff --git a/Sources/PlaybookUI/Internal/Utilities/View.swift b/Sources/PlaybookUI/Internal/Utilities/View.swift
new file mode 100644
index 0000000..9ec977d
--- /dev/null
+++ b/Sources/PlaybookUI/Internal/Utilities/View.swift
@@ -0,0 +1,26 @@
+import SwiftUI
+
+@available(iOS 15.0, *)
+internal extension View {
+ func textStyle(
+ font: Font,
+ color: Color = Color(.label),
+ alignment: TextAlignment = .leading,
+ lineLimit: Int? = nil
+ ) -> some View {
+ foregroundStyle(color)
+ .font(font)
+ .multilineTextAlignment(alignment)
+ .lineLimit(lineLimit)
+ .dynamicTypeSize(.large)
+ }
+
+ func imageStyle(
+ font: Font,
+ color: Color = Color(.label)
+ ) -> some View {
+ foregroundStyle(color)
+ .font(font)
+ .dynamicTypeSize(.large)
+ }
+}
diff --git a/Sources/PlaybookUI/Internal/Views/CatalogBottomBar.swift b/Sources/PlaybookUI/Internal/Views/CatalogBottomBar.swift
new file mode 100644
index 0000000..759e080
--- /dev/null
+++ b/Sources/PlaybookUI/Internal/Views/CatalogBottomBar.swift
@@ -0,0 +1,63 @@
+import SwiftUI
+
+@available(iOS 15.0, *)
+internal struct CatalogBottomBar: View {
+ let title: String?
+ let primaryItemSymbol: Image.SFSymbols
+
+ @EnvironmentObject
+ private var catalogState: CatalogState
+ @EnvironmentObject
+ private var shareState: ShareState
+
+ var body: some View {
+ VStack(spacing: 0) {
+ Divider()
+ .ignoresSafeArea()
+
+ HStack(spacing: 0) {
+ Button {
+ catalogState.isSearchPainCollapsed.toggle()
+ } label: {
+ Image(symbol: primaryItemSymbol)
+ .imageStyle(font: .title2)
+ .padding(8)
+ }
+
+ Spacer.fixed(length: 8)
+
+ if catalogState.selected != nil {
+ Button {
+ shareState.shareSnapshot()
+ } label: {
+ Image(symbol: .ellipsisCircle)
+ .imageStyle(font: .title2)
+ .padding(8)
+ }
+ }
+
+ Spacer(minLength: 0)
+
+ if let title {
+ Text(title)
+ .textStyle(
+ font: .headline,
+ lineLimit: 1
+ )
+ .minimumScaleFactor(0.5)
+ .padding(.horizontal, 8)
+ }
+
+ Spacer(minLength: 8)
+
+ ColorSchemePicker(colorScheme: $catalogState.colorScheme)
+ }
+ .padding(.horizontal, 8)
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ }
+ .frame(height: 56)
+ .background {
+ MaterialView()
+ }
+ }
+}
diff --git a/Sources/PlaybookUI/Internal/Views/CatalogDrawer.swift b/Sources/PlaybookUI/Internal/Views/CatalogDrawer.swift
new file mode 100644
index 0000000..ee815bf
--- /dev/null
+++ b/Sources/PlaybookUI/Internal/Views/CatalogDrawer.swift
@@ -0,0 +1,59 @@
+import Playbook
+import SwiftUI
+
+@available(iOS 15.0, *)
+internal struct CatalogDrawer: View {
+ @EnvironmentObject
+ private var catalogState: CatalogState
+
+ var body: some View {
+ ZStack {
+ CatalogTop()
+ Drawer(isCollapsed: $catalogState.isSearchPainCollapsed)
+ }
+ .onChange(of: catalogState.selected?.id) { _ in
+ catalogState.isSearchPainCollapsed = true
+ }
+ }
+}
+
+@available(iOS 15.0, *)
+private struct Drawer: View {
+ @Binding
+ var isCollapsed: Bool
+
+ var body: some View {
+ GeometryReader { geometry in
+ let drawerWidth =
+ geometry.safeAreaInsets.leading
+ + min(
+ geometry.size.width * 0.8,
+ max(geometry.size.width, geometry.size.height) * 0.5
+ )
+
+ ZStack {
+ Color.black
+ .opacity(isCollapsed ? 0 : 0.3)
+ .ignoresSafeArea()
+ .onTapGesture {
+ isCollapsed = true
+ }
+
+ HStack(spacing: 0) {
+ CatalogSearchPane()
+ .frame(width: drawerWidth)
+ .background {
+ Rectangle()
+ .ignoresSafeArea()
+ .shadow(radius: 10)
+ }
+
+ Spacer.zero
+ }
+ .offset(x: isCollapsed ? -geometry.size.width : 0)
+ }
+ .animation(.smooth(duration: 0.3), value: isCollapsed)
+ .frame(width: geometry.size.width, height: geometry.size.height)
+ }
+ }
+}
diff --git a/Sources/PlaybookUI/Internal/Views/CatalogKindRow.swift b/Sources/PlaybookUI/Internal/Views/CatalogKindRow.swift
new file mode 100644
index 0000000..90363b8
--- /dev/null
+++ b/Sources/PlaybookUI/Internal/Views/CatalogKindRow.swift
@@ -0,0 +1,42 @@
+import SwiftUI
+
+@available(iOS 15, *)
+internal struct CatalogKindRow: View {
+ let data: SearchedKindData
+ let isExpanded: Bool
+ let onSelect: () -> Void
+
+ var body: some View {
+ Button(action: onSelect) {
+ VStack(spacing: 0) {
+ HStack(spacing: 0) {
+ Image(uiImage: .logoMark)
+ .resizable()
+ .scaledToFit()
+ .frame(height: 16)
+
+ Spacer.fixed(length: 8)
+
+ HighlightText(
+ content: data.kind.rawValue,
+ range: data.highlightRange
+ )
+ .textStyle(font: .headline)
+
+ Spacer(minLength: 8)
+
+ Image(symbol: .chevronRight)
+ .imageStyle(
+ font: .caption,
+ color: Color(.secondaryLabel)
+ )
+ .rotationEffect(.radians(isExpanded ? .pi / 2 : 0))
+ }
+ .padding(16)
+
+ Separator(height: 1)
+ }
+ }
+ .buttonStyle(.borderless)
+ }
+}
diff --git a/Sources/PlaybookUI/Internal/Views/CatalogScenarioRow.swift b/Sources/PlaybookUI/Internal/Views/CatalogScenarioRow.swift
new file mode 100644
index 0000000..ad839b0
--- /dev/null
+++ b/Sources/PlaybookUI/Internal/Views/CatalogScenarioRow.swift
@@ -0,0 +1,37 @@
+import SwiftUI
+
+@available(iOS 15, *)
+internal struct CatalogScenarioRow: View {
+ let data: SearchedData
+ let isSelected: Bool
+ let onSelect: () -> Void
+
+ var body: some View {
+ Button(action: onSelect) {
+ VStack(spacing: 0) {
+ HStack(spacing: 0) {
+ Image(symbol: .circleFill)
+ .imageStyle(
+ font: .system(size: 8),
+ color: Color(isSelected ? .primaryBlue : .translucentFill)
+ )
+
+ Spacer.fixed(length: 8)
+
+ HighlightText(
+ content: data.scenario.name.rawValue,
+ range: data.highlightRange
+ )
+ .textStyle(font: .subheadline)
+
+ Spacer(minLength: 8)
+ }
+ .padding(16)
+
+ Separator(height: 1)
+ }
+ .padding(.leading, 16)
+ }
+ .buttonStyle(.borderless)
+ }
+}
diff --git a/Sources/PlaybookUI/Internal/Views/CatalogSearchPane.swift b/Sources/PlaybookUI/Internal/Views/CatalogSearchPane.swift
new file mode 100644
index 0000000..7a1202c
--- /dev/null
+++ b/Sources/PlaybookUI/Internal/Views/CatalogSearchPane.swift
@@ -0,0 +1,92 @@
+import SwiftUI
+
+@available(iOS 15.0, *)
+internal struct CatalogSearchPane: View {
+ @EnvironmentObject
+ private var searchState: SearchState
+ @EnvironmentObject
+ private var catalogState: CatalogState
+ @FocusState
+ private var isFocused
+
+ var body: some View {
+ VStack(spacing: 0) {
+ Spacer.fixed(length: 16)
+
+ SearchBar(text: $searchState.query)
+ .focused($isFocused)
+ Counter(
+ count: searchState.result.count,
+ total: searchState.result.total
+ )
+
+ Spacer.fixed(length: 16)
+
+ Divider()
+
+ List {
+ Group {
+ if searchState.result.kinds.isEmpty {
+ UnavailableView(
+ symbol: .magnifyingglass,
+ description: "No Result for \"\(searchState.query)\""
+ )
+ }
+ else {
+ ForEach(searchState.result.kinds, id: \.kind) { data in
+ let isExpanded = catalogState.currentExpandedKinds.contains(data.kind)
+
+ CatalogKindRow(data: data, isExpanded: isExpanded) {
+ withAnimation(.smooth(duration: 0.1)) {
+ if isExpanded {
+ catalogState.currentExpandedKinds.remove(data.kind)
+ }
+ else {
+ catalogState.currentExpandedKinds.insert(data.kind)
+ }
+ }
+ }
+
+ if isExpanded {
+ ForEach(data.scenarios, id: \.scenario.name) { data in
+ let select = SelectData(
+ kind: data.kind,
+ scenario: data.scenario
+ )
+
+ CatalogScenarioRow(
+ data: data,
+ isSelected: catalogState.selected?.id == select.id
+ ) {
+ catalogState.selected = select
+ }
+ }
+ }
+ }
+ }
+
+ Spacer.fixed(length: 16)
+ }
+ .listRowSpacing(.zero)
+ .listRowInsets(EdgeInsets())
+ .listRowSeparator(.hidden)
+ .listRowBackground(Color.clear)
+ }
+ .listStyle(.plain)
+ .environment(\.defaultMinListRowHeight, 0)
+ }
+ .background {
+ Color(.secondaryBackground)
+ .ignoresSafeArea()
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .onChange(of: searchState.query) { query in
+ catalogState.updateSearchingKinds(query: query, searchResult: searchState.result)
+ }
+ .onChange(of: catalogState.isSearchPainCollapsed) { isCollapsed in
+ if isCollapsed {
+ isFocused = false
+ }
+ }
+ }
+}
diff --git a/Sources/PlaybookUI/Internal/Views/CatalogSplit.swift b/Sources/PlaybookUI/Internal/Views/CatalogSplit.swift
new file mode 100644
index 0000000..523ccac
--- /dev/null
+++ b/Sources/PlaybookUI/Internal/Views/CatalogSplit.swift
@@ -0,0 +1,35 @@
+import Playbook
+import SwiftUI
+
+@available(iOS 15.0, *)
+internal struct CatalogSplit: View {
+ @EnvironmentObject
+ private var catalogState: CatalogState
+
+ var body: some View {
+ GeometryReader { geometry in
+ let sidebarWidth = geometry.size.width * 0.4
+
+ ZStack(alignment: .leading) {
+ HStack(spacing: 0) {
+ CatalogSearchPane()
+ Divider()
+ .ignoresSafeArea()
+ }
+ .frame(width: sidebarWidth)
+ .offset(x: catalogState.isSearchPainCollapsed ? -sidebarWidth / 2 : 0)
+
+ HStack(spacing: 0) {
+ Spacer.fixed(length: catalogState.isSearchPainCollapsed ? 0 : sidebarWidth)
+ CatalogTop()
+ .transformEnvironment(\.horizontalSizeClass) { sizeClass in
+ if !catalogState.isSearchPainCollapsed {
+ sizeClass = .compact
+ }
+ }
+ }
+ }
+ }
+ .animation(.smooth(duration: 0.3), value: catalogState.isSearchPainCollapsed)
+ }
+}
diff --git a/Sources/PlaybookUI/Internal/Views/CatalogTop.swift b/Sources/PlaybookUI/Internal/Views/CatalogTop.swift
new file mode 100644
index 0000000..d32df1a
--- /dev/null
+++ b/Sources/PlaybookUI/Internal/Views/CatalogTop.swift
@@ -0,0 +1,38 @@
+import SwiftUI
+
+@available(iOS 15.0, *)
+internal struct CatalogTop: View {
+ @EnvironmentObject
+ private var catalogState: CatalogState
+ @EnvironmentObject
+ private var shareState: ShareState
+
+ var body: some View {
+ Group {
+ if let selected = catalogState.selected {
+ ScenarioContentView(
+ scenario: selected.scenario,
+ additionalSafeAreaInsets: UIEdgeInsets(
+ top: .zero,
+ left: .zero,
+ bottom: 56,
+ right: .zero
+ ),
+ shareState: shareState
+ )
+ }
+ else {
+ UnavailableView(
+ symbol: .docTextMagnifyingglass,
+ description: "No Scenarios found"
+ )
+ }
+ }
+ .frame(maxHeight: .infinity)
+ .background {
+ Color(.primaryBackground)
+ .ignoresSafeArea()
+ }
+ .ignoresSafeArea()
+ }
+}
diff --git a/Sources/PlaybookUI/Internal/Views/ColorSchemePicker.swift b/Sources/PlaybookUI/Internal/Views/ColorSchemePicker.swift
new file mode 100644
index 0000000..c2a0282
--- /dev/null
+++ b/Sources/PlaybookUI/Internal/Views/ColorSchemePicker.swift
@@ -0,0 +1,32 @@
+import SwiftUI
+
+@available(iOS 14.0, *)
+internal struct ColorSchemePicker: View {
+ @Binding
+ var colorScheme: ColorScheme?
+ @Environment(\.colorScheme)
+ private var systemColorScheme
+
+ var body: some View {
+ Picker(
+ selection: Binding(
+ get: { colorScheme ?? systemColorScheme },
+ set: { colorScheme = $0 }
+ ),
+ content: {
+ ForEach(ColorScheme.allCases, id: \.self) { colorScheme in
+ switch colorScheme {
+ case .light: Image(symbol: .sunMax)
+ case .dark: Image(symbol: .moon)
+ @unknown default: EmptyView()
+ }
+ }
+ },
+ label: {
+ EmptyView()
+ }
+ )
+ .pickerStyle(.segmented)
+ .fixedSize()
+ }
+}
diff --git a/Sources/PlaybookUI/Internal/Views/Counter.swift b/Sources/PlaybookUI/Internal/Views/Counter.swift
new file mode 100644
index 0000000..cf9c3a7
--- /dev/null
+++ b/Sources/PlaybookUI/Internal/Views/Counter.swift
@@ -0,0 +1,20 @@
+import SwiftUI
+
+@available(iOS 15.0, *)
+internal struct Counter: View {
+ let count: Int
+ let total: Int
+
+ var body: some View {
+ HStack(spacing: 0) {
+ Spacer.zero
+
+ Text("\(count) / \(total)")
+ .textStyle(
+ font: .caption.monospacedDigit(),
+ color: Color(.secondaryLabel)
+ )
+ }
+ .padding(.horizontal, 24)
+ }
+}
diff --git a/Sources/PlaybookUI/Internal/Views/GalleryDetail.swift b/Sources/PlaybookUI/Internal/Views/GalleryDetail.swift
new file mode 100644
index 0000000..7cb149b
--- /dev/null
+++ b/Sources/PlaybookUI/Internal/Views/GalleryDetail.swift
@@ -0,0 +1,34 @@
+import SwiftUI
+
+@available(iOS 15.0, *)
+internal struct GalleryDetail: View {
+ let data: SelectData
+
+ @StateObject
+ private var shareState = ShareState()
+
+ var body: some View {
+ ZStack {
+ ScenarioContentView(
+ scenario: data.scenario,
+ additionalSafeAreaInsets: UIEdgeInsets(
+ top: 56,
+ left: .zero,
+ bottom: .zero,
+ right: .zero
+ ),
+ shareState: shareState
+ )
+ .ignoresSafeArea()
+ }
+ .safeAreaInset(edge: .top, spacing: 0) {
+ GalleryDetailTopBar(title: data.scenario.name.rawValue) {
+ shareState.shareSnapshot()
+ }
+ }
+ .background {
+ Color(.primaryBackground)
+ .ignoresSafeArea()
+ }
+ }
+}
diff --git a/Sources/PlaybookUI/Internal/Views/GalleryDetailTopBar.swift b/Sources/PlaybookUI/Internal/Views/GalleryDetailTopBar.swift
new file mode 100644
index 0000000..0e999b9
--- /dev/null
+++ b/Sources/PlaybookUI/Internal/Views/GalleryDetailTopBar.swift
@@ -0,0 +1,60 @@
+import SwiftUI
+
+@available(iOS 15.0, *)
+internal struct GalleryDetailTopBar: View {
+ let title: String
+ let share: () -> Void
+
+ var body: some View {
+ VStack(spacing: 0) {
+ HStack(spacing: 0) {
+ Button(action: share) {
+ Image(symbol: .ellipsisCircle)
+ .imageStyle(font: .title2)
+ }
+
+ Spacer(minLength: 16)
+
+ Text(title)
+ .textStyle(font: .headline, lineLimit: 1)
+
+ Spacer(minLength: 16)
+
+ CloseButton()
+ }
+ .padding(.horizontal, 16)
+ .frame(maxHeight: .infinity)
+
+ Divider()
+ .ignoresSafeArea()
+ }
+ .frame(height: 56)
+ .background {
+ MaterialView()
+ }
+ }
+}
+
+@available(iOS 15.0, *)
+private struct CloseButton: View {
+ @Environment(\.dismiss)
+ private var dismiss
+
+ var body: some View {
+ Button {
+ dismiss()
+ } label: {
+ ZStack {
+ Circle()
+ .fill(Color(.translucentFill))
+ .frame(width: 30, height: 30)
+
+ Image(symbol: .xmark)
+ .imageStyle(
+ font: .subheadline.weight(.bold),
+ color: Color(.secondaryLabel)
+ )
+ }
+ }
+ }
+}
diff --git a/Sources/PlaybookUI/Internal/Views/GalleryKindRow.swift b/Sources/PlaybookUI/Internal/Views/GalleryKindRow.swift
new file mode 100644
index 0000000..546b556
--- /dev/null
+++ b/Sources/PlaybookUI/Internal/Views/GalleryKindRow.swift
@@ -0,0 +1,50 @@
+import SwiftUI
+
+@available(iOS 15.0, *)
+internal struct GalleryKindRow: View {
+ let data: SearchedKindData
+ let onSelect: (SearchedData) -> Void
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 0) {
+ HStack(spacing: 8) {
+ Image(uiImage: .logoMark)
+ .resizable()
+ .scaledToFit()
+ .frame(height: 16)
+
+ HighlightText(
+ content: data.kind.rawValue,
+ range: data.highlightRange
+ )
+ .textStyle(font: .headline)
+ }
+ .padding(.top, 16)
+ .padding(.horizontal, 24)
+
+ ScrollView(.horizontal, showsIndicators: false) {
+ LazyHStack(alignment: .top, spacing: 0) {
+ ForEach(data.scenarios, id: \.scenario.name) { data in
+ Button {
+ onSelect(data)
+ } label: {
+ GalleryThumbnail(data: data)
+ }
+ .buttonStyle(ScaleButtonStyle())
+ }
+ }
+ .padding(12)
+ }
+
+ Separator(height: 2)
+ }
+ }
+}
+
+private struct ScaleButtonStyle: ButtonStyle {
+ func makeBody(configuration: Configuration) -> some View {
+ configuration.label
+ .scaleEffect(configuration.isPressed ? 0.9 : 1)
+ .animation(.snappy(duration: 0.2), value: configuration.isPressed)
+ }
+}
diff --git a/Sources/PlaybookUI/Internal/Views/GalleryThumbnail.swift b/Sources/PlaybookUI/Internal/Views/GalleryThumbnail.swift
new file mode 100644
index 0000000..a6ed9a2
--- /dev/null
+++ b/Sources/PlaybookUI/Internal/Views/GalleryThumbnail.swift
@@ -0,0 +1,114 @@
+import Playbook
+import SwiftUI
+
+@available(iOS 15.0, *)
+internal struct GalleryThumbnail: View {
+ let data: SearchedData
+
+ @State
+ private var image: UIImage?
+ @EnvironmentObject
+ private var imageLoader: ImageLoader
+ @Environment(\.colorScheme)
+ private var colorScheme
+ private let contentScale: CGFloat = 0.3
+ private let imageScale: CGFloat = 0.5
+ private let screenSize = UIScreen.main.fixedCoordinateSpace.bounds.size
+ private let cornerRadius: CGFloat = 16
+
+ var body: some View {
+ ZStack {
+ Color(.primaryBackground)
+
+ if let image {
+ Image(uiImage: image)
+ .resizable()
+ .frame(
+ width: image.size.width * contentScale / imageScale,
+ height: image.size.height * contentScale / imageScale
+ )
+ }
+ else {
+ Placeholder()
+ }
+ }
+ .frame(width: contentWidth, height: contentHeight, alignment: .top)
+ .overlay(alignment: .bottom) {
+ NameLabel(data: data)
+ }
+ .overlay {
+ RoundedRectangle(cornerRadius: cornerRadius)
+ .strokeBorder(Color(.systemGray5), lineWidth: 4)
+ }
+ .cornerRadius(cornerRadius)
+ .padding(4)
+ .onChange(of: colorScheme) { _ in
+ image = nil
+ }
+ .task(id: colorScheme) {
+ let source = ImageSource(
+ scenario: data.scenario,
+ kind: data.kind,
+ size: screenSize,
+ scale: imageScale,
+ colorScheme: colorScheme
+ )
+ image = await imageLoader.loadImage(for: source)
+ }
+ }
+}
+
+@available(iOS 15, *)
+private struct NameLabel: View {
+ let data: SearchedData
+
+ var body: some View {
+ VStack(spacing: 0) {
+ Divider()
+ .ignoresSafeArea()
+
+ HighlightText(
+ content: data.scenario.name.rawValue,
+ range: data.highlightRange
+ )
+ .textStyle(font: .caption, lineLimit: 3)
+ .padding(.top, 4)
+ .padding([.bottom, .horizontal], 8)
+ .frame(maxWidth: .infinity)
+ }
+ .background {
+ MaterialView()
+ }
+ }
+}
+
+private struct Placeholder: View {
+ @State
+ private var isAnimating = false
+
+ var body: some View {
+ Color(.translucentFill)
+ .opacity(isAnimating ? 1 : 0)
+ .animation(
+ .linear(duration: 0.5)
+ .repeatForever(autoreverses: true),
+ value: isAnimating
+ )
+ .onAppear {
+ if !isAnimating {
+ isAnimating = true
+ }
+ }
+ }
+}
+
+@available(iOS 15.0, *)
+private extension GalleryThumbnail {
+ var contentWidth: CGFloat {
+ screenSize.width * contentScale
+ }
+
+ var contentHeight: CGFloat {
+ screenSize.height * contentScale
+ }
+}
diff --git a/Sources/PlaybookUI/Internal/Views/HighlightText.swift b/Sources/PlaybookUI/Internal/Views/HighlightText.swift
new file mode 100644
index 0000000..66a5dd2
--- /dev/null
+++ b/Sources/PlaybookUI/Internal/Views/HighlightText.swift
@@ -0,0 +1,30 @@
+import SwiftUI
+
+@available(iOS 15, *)
+internal struct HighlightText: View {
+ let content: String
+ let range: Range?
+
+ var body: some View {
+ if let range {
+ Text(attributedContent(range: range))
+ }
+ else {
+ Text(content)
+ }
+ }
+}
+
+@available(iOS 15, *)
+private extension HighlightText {
+ func attributedContent(range: Range) -> AttributedString {
+ var attributed = AttributedString(content)
+ let nsRange = NSRange(range, in: content)
+
+ if let attributedRange = Range(nsRange, in: attributed) {
+ attributed[attributedRange].foregroundColor = .init(uiColor: .highlight)
+ }
+
+ return attributed
+ }
+}
diff --git a/Sources/PlaybookUI/Internal/Views/MaterialView.swift b/Sources/PlaybookUI/Internal/Views/MaterialView.swift
new file mode 100644
index 0000000..6f715d5
--- /dev/null
+++ b/Sources/PlaybookUI/Internal/Views/MaterialView.swift
@@ -0,0 +1,10 @@
+import SwiftUI
+
+@available(iOS 15.0, *)
+internal struct MaterialView: View {
+ var body: some View {
+ Rectangle()
+ .fill(Material.bar)
+ .ignoresSafeArea()
+ }
+}
diff --git a/Sources/PlaybookUI/Internal/Views/ScenarioContentView.swift b/Sources/PlaybookUI/Internal/Views/ScenarioContentView.swift
new file mode 100644
index 0000000..6136089
--- /dev/null
+++ b/Sources/PlaybookUI/Internal/Views/ScenarioContentView.swift
@@ -0,0 +1,24 @@
+import Playbook
+import SwiftUI
+
+@available(iOS 14.0, *)
+internal struct ScenarioContentView: UIViewControllerRepresentable {
+ let scenario: Scenario
+ let additionalSafeAreaInsets: UIEdgeInsets
+ let shareState: ShareState
+
+ func makeUIViewController(context: Context) -> ScenarioViewController {
+ let context = ScenarioContext(
+ snapshotWaiter: SnapshotWaiter(),
+ isSnapshot: false,
+ screenSize: UIScreen.main.bounds.size
+ )
+ return ScenarioViewController(context: context)
+ }
+
+ func updateUIViewController(_ uiViewController: ScenarioViewController, context: Context) {
+ shareState.scenarioViewController = uiViewController
+ uiViewController.scenario = scenario
+ uiViewController.additionalSafeAreaInsets = additionalSafeAreaInsets
+ }
+}
diff --git a/Sources/PlaybookUI/Internal/Views/SearchBar.swift b/Sources/PlaybookUI/Internal/Views/SearchBar.swift
new file mode 100644
index 0000000..a16e2d3
--- /dev/null
+++ b/Sources/PlaybookUI/Internal/Views/SearchBar.swift
@@ -0,0 +1,78 @@
+import SwiftUI
+import UIKit
+
+@available(iOS 15.0, *)
+internal struct SearchBar: View {
+ @Binding
+ var text: String
+ @FocusState
+ private var isFocused
+ @State
+ private var isEditing = false
+
+ var body: some View {
+ HStack(spacing: 0) {
+ HStack(spacing: 0) {
+ Image(symbol: .magnifyingglass)
+ .imageStyle(
+ font: .headline.weight(.regular),
+ color: Color(.secondaryLabel)
+ )
+ .padding(.trailing, 4)
+ .onTapGesture {
+ isFocused = true
+ }
+
+ TextField(
+ text: $text,
+ prompt: Text("Search")
+ .foregroundColor(Color(.secondaryLabel))
+ ) {
+ EmptyView()
+ }
+ .textFieldStyle(.plain)
+ .focused($isFocused)
+
+ if !text.isEmpty {
+ Button {
+ text = ""
+ } label: {
+ Image(symbol: .xmarkCircleFill)
+ .foregroundStyle(.gray)
+ .dynamicTypeSize(.large)
+ }
+ .transition(.opacity)
+ }
+ }
+ .padding(.vertical, 12)
+ .padding(.horizontal, 16)
+ .background {
+ Rectangle()
+ .fill(Color(.tertiarySystemFill))
+ .clipShape(.capsule)
+ }
+
+ if isEditing {
+ Spacer.fixed(length: 8)
+
+ Button("Cancel") {
+ isFocused = false
+ }
+ .buttonStyle(.borderless)
+ .foregroundColor(Color(.primaryBlue))
+ .transition(
+ .move(edge: .trailing)
+ .combined(with: .opacity)
+ )
+ }
+ }
+ .padding(.vertical, 4)
+ .padding(.horizontal, 16)
+ .clipped()
+ .dynamicTypeSize(.large)
+ .animation(.easeOut(duration: 0.2), value: isEditing)
+ .onChange(of: isFocused) { isFocused in
+ isEditing = isFocused
+ }
+ }
+}
diff --git a/Sources/PlaybookUI/Internal/Views/Separator.swift b/Sources/PlaybookUI/Internal/Views/Separator.swift
new file mode 100644
index 0000000..1d8d69b
--- /dev/null
+++ b/Sources/PlaybookUI/Internal/Views/Separator.swift
@@ -0,0 +1,13 @@
+import SwiftUI
+
+internal struct Separator: View {
+ let height: CGFloat
+
+ var body: some View {
+ Rectangle()
+ .fill(Color(.translucentFill))
+ .frame(height: height)
+ .padding(.leading, 16)
+ .fixedSize(horizontal: false, vertical: true)
+ }
+}
diff --git a/Sources/PlaybookUI/Internal/Views/UnavailableView.swift b/Sources/PlaybookUI/Internal/Views/UnavailableView.swift
new file mode 100644
index 0000000..539ee7f
--- /dev/null
+++ b/Sources/PlaybookUI/Internal/Views/UnavailableView.swift
@@ -0,0 +1,24 @@
+import SwiftUI
+
+@available(iOS 15.0, *)
+internal struct UnavailableView: View {
+ let symbol: Image.SFSymbols
+ let description: String
+
+ var body: some View {
+ VStack(spacing: 16) {
+ Image(symbol: symbol)
+ .imageStyle(
+ font: .largeTitle,
+ color: Color(.secondaryLabel)
+ )
+
+ Text(description)
+ .textStyle(font: .headline)
+ .fixedSize(horizontal: false, vertical: true)
+ .frame(maxWidth: .infinity)
+ }
+ .padding(.vertical, 40)
+ .padding(.horizontal, 24)
+ }
+}
diff --git a/Sources/PlaybookUI/Internal/WeakReference.swift b/Sources/PlaybookUI/Internal/WeakReference.swift
deleted file mode 100644
index b9b832e..0000000
--- a/Sources/PlaybookUI/Internal/WeakReference.swift
+++ /dev/null
@@ -1,13 +0,0 @@
-@propertyWrapper
-internal final class WeakReference {
- var wrappedValue: Value? {
- get { value }
- set { value = newValue }
- }
-
- private weak var value: Value?
-
- init(wrappedValue: Value?) {
- value = wrappedValue
- }
-}
diff --git a/Sources/PlaybookUI/PlaybookCatalog.swift b/Sources/PlaybookUI/PlaybookCatalog.swift
index 848d0c9..995b9f6 100644
--- a/Sources/PlaybookUI/PlaybookCatalog.swift
+++ b/Sources/PlaybookUI/PlaybookCatalog.swift
@@ -1,195 +1,65 @@
+import Playbook
import SwiftUI
-/// A view that displays scenarios manged by given `Playbook` instance with
-/// catalog-style appearance.
+@available(iOS 15.0, *)
public struct PlaybookCatalog: View {
- private var underlyingView: PlaybookCatalogInternal
+ private let title: String?
+
+ @StateObject
+ private var searchState: SearchState
+ @StateObject
+ private var catalogState = CatalogState()
+ @StateObject
+ private var shareState = ShareState()
+ @Environment(\.horizontalSizeClass)
+ private var horizontalSizeClass
+ @Environment(\.verticalSizeClass)
+ private var verticalSizeClass
- /// Creates a new view that displays scenarios managed by given `Playbook` instance.
- ///
- /// - Parameters:
- /// - name: A name of `Playbook` to be displayed on the user interface.
- /// - playbook: A `Playbook` instance that manages scenarios to be displayed.
public init(
- name: String = "PLAYBOOK",
+ title: String? = nil,
playbook: Playbook = .default
) {
- underlyingView = PlaybookCatalogInternal(
- name: name,
- playbook: playbook,
- store: CatalogStore(playbook: playbook)
- )
+ self.title = title
+ self._searchState = StateObject(wrappedValue: SearchState(playbook: playbook))
}
- /// Declares the content and behavior of this view.
public var body: some View {
- underlyingView
- }
-}
+ Group {
+ switch (horizontalSizeClass, verticalSizeClass) {
+ case (.regular, .regular):
+ CatalogSplit()
-internal struct PlaybookCatalogInternal: View {
- var name: String
- var playbook: Playbook
-
- @ObservedObject
- var store: CatalogStore
-
- @WeakReference
- var contentUIView: UIView?
-
- @Environment(\.horizontalSizeClass)
- var horizontalSizeClass
-
- @Environment(\.verticalSizeClass)
- var verticalSizeClass
-
- var body: some View {
- platformContent()
- .environmentObject(store)
- .onAppear(perform: selectFirstScenario)
- .sheet(item: $store.shareItem) { item in
- ImageSharingView(item: item) { self.store.shareItem = nil }
- .edgesIgnoringSafeArea(.all)
+ default:
+ CatalogDrawer()
}
- }
-}
-
-private extension PlaybookCatalogInternal {
- var bottomBarHeight: CGFloat { 44 }
-
- func platformContent() -> some View {
- switch (horizontalSizeClass, verticalSizeClass) {
- case (.regular, .regular):
- return AnyView(
- CatalogSplitStyle(
- name: name,
- searchTree: ScenarioSearchTree(),
- content: scenarioContent
- )
- )
-
- default:
- return AnyView(
- CatalogDrawerStyle(
- name: name,
- searchTree: ScenarioSearchTree(),
- content: scenarioContent
- )
- )
}
- }
-
- func displayView() -> some View {
- if let data = store.selectedScenario {
- return AnyView(
- ScenarioContentView(
- kind: data.kind,
- scenario: data.scenario,
- additionalSafeAreaInsets: .only(bottom: bottomBarHeight),
- contentUIView: _contentUIView
- )
- .edgesIgnoringSafeArea(.all)
+ .safeAreaInset(edge: .bottom, spacing: 0) {
+ CatalogBottomBar(
+ title: title,
+ primaryItemSymbol: primaryBarItemSymbol
)
}
- else {
- return AnyView(emptyContent())
+ .ignoresSafeArea(.keyboard)
+ .preferredColorScheme(catalogState.colorScheme)
+ .environmentObject(searchState)
+ .environmentObject(catalogState)
+ .environmentObject(shareState)
+ .onAppear {
+ catalogState.selectInitial(searchResult: searchState.result)
}
}
+}
- func emptyContent() -> some View {
- VStack(spacing: 0) {
- HStack {
- Spacer.zero
- }
-
- Spacer.zero
-
- Image(symbol: .book)
- .imageScale(.large)
- .font(.system(size: 60))
- .foregroundColor(Color(.label))
-
- Spacer.fixed(length: 44)
-
- Text("There are no scenarios")
- .foregroundColor(Color(.label))
- .font(.system(size: 24, weight: .bold))
- .lineLimit(nil)
-
- Spacer.zero
- }
- .padding(.horizontal, 24)
- }
-
- func scenarioContent(firstBarItem: CatalogBarItem) -> some View {
- ZStack {
- Color(.scenarioBackground)
- .edgesIgnoringSafeArea(.all)
-
- displayView()
-
- VStack(spacing: 0) {
- Spacer.zero
-
- Divider()
- .edgesIgnoringSafeArea(.all)
-
- bottomBar(firstBarItem: firstBarItem)
- }
- }
- }
-
- func bottomBar(firstBarItem: CatalogBarItem) -> some View {
- HStack(spacing: 24) {
- firstBarItem
-
- if store.selectedScenario != nil {
- CatalogBarItem(
- image: Image(symbol: .squareAndArrowUp),
- insets: .only(bottom: 4),
- action: share
- )
- }
-
- HStack(spacing: 0) {
- Spacer(minLength: 0)
-
- Text(name)
- .bold()
- .lineLimit(1)
- .font(.system(size: 24))
- }
- }
- .padding(.horizontal, 24)
- .frame(height: bottomBarHeight)
- .background(
- Blur(style: .systemMaterial)
- .scaledToFill()
- .edgesIgnoringSafeArea(.all),
- alignment: .topLeading
- )
- }
-
- func share() {
- guard let uiView = contentUIView else { return }
-
- let image = UIGraphicsImageRenderer(bounds: uiView.bounds).image { _ in
- uiView.drawHierarchy(in: uiView.bounds, afterScreenUpdates: true)
- }
-
- store.shareItem = ImageSharingView.Item(image: image)
- }
+@available(iOS 15.0, *)
+private extension PlaybookCatalog {
+ var primaryBarItemSymbol: Image.SFSymbols {
+ switch (horizontalSizeClass, verticalSizeClass) {
+ case (.regular, .regular):
+ return .sidebarLeft
- func selectFirstScenario() {
- guard store.selectedScenario == nil, let store = playbook.stores.first, let scenario = store.scenarios.first else {
- return
+ default:
+ return .magnifyingglass
}
-
- self.store.start()
- self.store.selectedScenario = SearchedData(
- scenario: scenario,
- kind: store.kind,
- shouldHighlight: false
- )
}
}
diff --git a/Sources/PlaybookUI/PlaybookGallery.swift b/Sources/PlaybookUI/PlaybookGallery.swift
index f9f8222..c693f9e 100644
--- a/Sources/PlaybookUI/PlaybookGallery.swift
+++ b/Sources/PlaybookUI/PlaybookGallery.swift
@@ -1,342 +1,110 @@
+import Playbook
import SwiftUI
-/// A view that displays scenarios manged by given `Playbook` instance with
-/// gallery-style appearance.
+@available(iOS 15.0, *)
public struct PlaybookGallery: View {
- private let name: String
- private let snapshotColorScheme: ColorScheme
- private let store: GalleryStore
+ private let title: String?
+
+ @StateObject
+ private var searchState: SearchState
+ @StateObject
+ private var galleryState = GalleryState()
+ @StateObject
+ private var imageLoader = ImageLoader()
+ @FocusState
+ private var isFocused
- /// Creates a new view that displays scenarios managed by given `Playbook` instance.
- ///
- /// - Parameters:
- /// - name: A name of `Playbook` to be displayed on the user interface.
- /// - playbook: A `Playbook` instance that manages scenarios to be displayed.
- /// - preSnapshotCountLimit: The limit on the number of snapshot images for preview
- /// that can be generated before being displayed.
- /// - snapshotColorScheme: The color scheme of the snapshot image for preview.
- ///
- /// - Note: If the displaying of this view is heavy, you can delay the generation
- /// of the snapshot image for preview by lowering `preSnapshotCountLimit`.
public init(
- name: String = "PLAYBOOK",
- playbook: Playbook = .default,
- preSnapshotCountLimit: Int = 100,
- snapshotColorScheme: ColorScheme = .light
+ title: String? = nil,
+ playbook: Playbook = .default
) {
- self.name = name
- self.snapshotColorScheme = snapshotColorScheme
- self.store = GalleryStore(
- playbook: playbook,
- preSnapshotCountLimit: preSnapshotCountLimit,
- screenSize: UIScreen.main.fixedCoordinateSpace.bounds.size,
- userInterfaceStyle: snapshotColorScheme.userInterfaceStyle
- )
+ self.title = title
+ self._searchState = StateObject(wrappedValue: SearchState(playbook: playbook))
}
- /// Declares the content and behavior of this view.
- @ViewBuilder
public var body: some View {
- #if swift(>=5.3)
- if #available(iOS 14.0, *) {
- PlaybookGalleryIOS14(
- name: name,
- snapshotColorScheme: snapshotColorScheme,
- store: store
- )
- }
- else {
- PlaybookGalleryIOS13(
- name: name,
- snapshotColorScheme: snapshotColorScheme,
- store: store
- )
- }
- #else
- PlaybookGalleryIOS13(
- name: name,
- snapshotColorScheme: snapshotColorScheme,
- store: store
- )
- #endif
- }
-}
-
-#if swift(>=5.3)
-@available(iOS 14.0, *)
-internal struct PlaybookGalleryIOS14: View {
- var name: String
- var snapshotColorScheme: ColorScheme
-
- @ObservedObject
- var store: GalleryStore
-
- @Environment(\.galleryDependency)
- var dependency
-
- var body: some View {
- GeometryReader { geometry in
- NavigationView {
- ScrollView {
- LazyVStack(spacing: .zero) {
- SearchBar(text: $store.searchText, height: 44)
- .padding(.leading, geometry.safeAreaInsets.leading)
- .padding(.trailing, geometry.safeAreaInsets.trailing)
+ NavigationView {
+ List {
+ Group {
+ Spacer.fixed(length: 16)
+ SearchBar(text: $searchState.query)
+ .focused($isFocused)
+
+ Counter(
+ count: searchState.result.count,
+ total: searchState.result.total
+ )
+ .onDisappear {
+ isFocused = false
+ }
- statefulBody(geometry: geometry)
+ if searchState.result.kinds.isEmpty {
+ UnavailableView(
+ symbol: .magnifyingglass,
+ description: "No Result for \"\(searchState.query)\""
+ )
}
- }
- .ignoresSafeArea(edges: .horizontal)
- .navigationBarTitle(name)
- .background(Color(.primaryBackground).ignoresSafeArea())
- .sheet(item: $store.selectedScenario) { data in
- ScenarioDisplaySheet(data: data) {
- store.selectedScenario = nil
+ else {
+ ForEach(searchState.result.kinds, id: \.kind) { data in
+ GalleryKindRow(data: data) { selected in
+ galleryState.selected = SelectData(
+ kind: selected.kind,
+ scenario: selected.scenario
+ )
+ }
+ }
}
- .environmentObject(store)
+
+ Rectangle()
+ .fill(.clear)
+ .frame(height: 24)
+ .frame(maxWidth: .infinity)
+ .contextMenu {
+ Button {
+ galleryState.clearImageCache()
+ } label: {
+ Text("Clear image cache")
+ }
+ }
}
+ .listRowSpacing(.zero)
+ .listRowInsets(EdgeInsets())
+ .listRowSeparator(.hidden)
+ .listRowBackground(Color.clear)
+ .transition(.identity)
}
- .environmentObject(store)
- .navigationViewStyle(StackNavigationViewStyle())
- .onAppear {
- dependency.scheduler.schedule(on: .main, action: store.prepare)
- }
- }
- }
-}
-
-@available(iOS 14.0, *)
-private extension PlaybookGalleryIOS14 {
- @ViewBuilder
- func statefulBody(geometry: GeometryProxy) -> some View {
- switch store.status {
- case .ready where store.result.data.isEmpty:
- message("This filter resulted in 0 results", font: .headline)
-
- case .ready:
- Counter(numerator: store.result.matchedCount, denominator: store.scenariosCount)
- .padding(.leading, geometry.safeAreaInsets.leading)
- .padding(.trailing, geometry.safeAreaInsets.trailing)
-
- ForEach(store.result.data, id: \.kind) { data in
- ScenarioDisplayList(
- data: data,
- safeAreaInsets: geometry.safeAreaInsets,
- serialDispatcher: SerialMainDispatcher(
- interval: 0.2,
- scheduler: dependency.scheduler
- ),
- onSelect: { store.selectedScenario = $0 }
- )
+ .listStyle(.plain)
+ .environment(\.defaultMinListRowHeight, 0)
+ .navigationTitleIfPresent(title)
+ .background {
+ Color(.primaryBackground)
+ .ignoresSafeArea()
}
-
- case .standby:
- VStack(spacing: .zero) {
- message("Preparing snapshots ...", font: .system(size: 24))
-
- Image(symbol: .book)
- .imageScale(.large)
- .font(.system(size: 60))
- .foregroundColor(Color(.label))
+ .ignoresSafeArea(.keyboard)
+ .sheet(item: $galleryState.selected) { data in
+ GalleryDetail(data: data)
}
- }
- }
-
- func message(_ text: String, font: Font) -> some View {
- Text(text)
- .foregroundColor(Color(.label))
- .font(font)
- .bold()
- .lineLimit(nil)
- .fixedSize(horizontal: false, vertical: true)
- .padding(.vertical, 44)
- .padding(.horizontal, 24)
- }
-}
-#endif
-
-internal struct PlaybookGalleryIOS13: View {
- var name: String
- var snapshotColorScheme: ColorScheme
-
- @ObservedObject
- var store: GalleryStore
-
- @Environment(\.galleryDependency)
- var dependency
-
- var body: some View {
- GeometryReader { geometry in
- NavigationView {
- TableView(
- animated: false,
- snapshot: self.snapshot(),
- configureUIView: self.configureTableview,
- row: { self.row(with: $0, geometry: geometry) }
- )
- .edgesIgnoringSafeArea(.all)
- .navigationBarTitle(self.name)
- .sheet(item: self.$store.selectedScenario) { data in
- ScenarioDisplaySheet(data: data) {
- self.store.selectedScenario = nil
- }
- .environmentObject(self.store)
+ .toolbar {
+ ToolbarItem(placement: .topBarTrailing) {
+ ColorSchemePicker(colorScheme: $galleryState.colorScheme)
}
}
- .environmentObject(self.store)
- .navigationViewStyle(StackNavigationViewStyle())
- .onAppear {
- self.dependency.scheduler.schedule(on: .main, action: self.store.prepare)
- }
}
+ .navigationViewStyle(.stack)
+ .preferredColorScheme(galleryState.colorScheme)
+ .environmentObject(imageLoader)
}
}
-private extension PlaybookGalleryIOS13 {
- enum Status {
- case standby
- case ready
- }
-
- struct Section: Hashable {}
-
- enum Row: Hashable {
- case scenarios(data: SearchedListData)
- case counter(numerator: Int, denominator: Int)
- case standby
- case empty
-
- func hash(into hasher: inout Hasher) {
- switch self {
- case .scenarios(let data):
- hasher.combine(data.kind)
-
- case .counter(let numerator, let denominator):
- hasher.combine(numerator)
- hasher.combine(denominator)
-
- case .standby:
- break
-
- case .empty:
- break
- }
- }
-
- static func == (lhs: Row, rhs: Row) -> Bool {
- switch (lhs, rhs) {
- case (.scenarios(let lhs), .scenarios(let rhs)):
- return lhs.kind == rhs.kind && lhs.shouldHighlight == rhs.shouldHighlight
-
- case (.counter(let lDenominator, let lNumerator), .counter(let rDenominator, let rNumerator)):
- return lDenominator == rDenominator && lNumerator == rNumerator
-
- case (.standby, .standby), (.empty, .empty):
- return true
-
- default:
- return false
- }
- }
- }
-
- func row(with row: Row, geometry: GeometryProxy) -> some View {
- switch row {
- case .scenarios(let data):
- return AnyView(
- ScenarioDisplayList(
- data: data,
- safeAreaInsets: geometry.safeAreaInsets,
- serialDispatcher: SerialMainDispatcher(
- interval: 0.2,
- scheduler: self.dependency.scheduler
- ),
- onSelect: { self.store.selectedScenario = $0 }
- )
- )
-
- case .counter(let numerator, let denominator):
- return AnyView(Counter(numerator: numerator, denominator: denominator))
-
- case .standby:
- return AnyView(standby())
-
- case .empty:
- return AnyView(message("This filter resulted in 0 results", font: .headline))
- }
- }
-
- func standby() -> some View {
- VStack(spacing: 0) {
- message("Preparing snapshots ...", font: .system(size: 24))
-
- Image(symbol: .book)
- .imageScale(.large)
- .font(.system(size: 60))
- .foregroundColor(Color(.label))
- }
- }
-
- func message(_ text: String, font: Font) -> some View {
- Text(text)
- .foregroundColor(Color(.label))
- .font(font)
- .bold()
- .lineLimit(nil)
- .fixedSize(horizontal: false, vertical: true)
- .padding(.vertical, 44)
- .padding(.horizontal, 24)
- }
-
- func snapshot() -> NSDiffableDataSourceSnapshot {
- var snapshot = NSDiffableDataSourceSnapshot()
- snapshot.appendSections([Section()])
-
- switch store.status {
- case .ready where store.result.data.isEmpty:
- snapshot.appendItems([.empty])
-
- case .ready:
- let counter = Row.counter(
- numerator: store.result.matchedCount,
- denominator: store.scenariosCount
- )
- snapshot.appendItems([counter] + store.result.data.map { .scenarios(data: $0) })
-
- case .standby:
- snapshot.appendItems([.standby])
+@available(iOS 14.0, *)
+private extension View {
+ @ViewBuilder
+ func navigationTitleIfPresent(_ title: S?) -> some View {
+ if let title {
+ navigationTitle(title)
}
-
- return snapshot
- }
-
- func configureTableview(_ tableView: UITableView) {
- let tableHeaderView = UIHostingController(
- rootView: SearchBar(text: $store.searchText, height: 44)
- )
- tableHeaderView.view.backgroundColor = .clear
- tableHeaderView.view.sizeToFit()
- tableView.backgroundColor = .primaryBackground
- tableView.separatorStyle = .none
- tableView.insetsContentViewsToSafeArea = false
- tableView.keyboardDismissMode = .onDrag
- tableView.estimatedRowHeight = ScenarioDisplay.scale * store.snapshotLoader.device.size.height + 100
- tableView.tableHeaderView = tableHeaderView.view
- tableView.tableFooterView = UIView()
- }
-}
-
-private extension ColorScheme {
- var userInterfaceStyle: UIUserInterfaceStyle {
- switch self {
- case .light:
- return .light
-
- case .dark:
- return .dark
-
- @unknown default:
- return .light
+ else {
+ self
}
}
}
diff --git a/Tests/SnapshotTests.swift b/Tests/SnapshotTests.swift
index 52cbf3a..1114c9b 100644
--- a/Tests/SnapshotTests.swift
+++ b/Tests/SnapshotTests.swift
@@ -3,7 +3,6 @@ import XCTest
final class SnapshotTests: XCTestCase {
func testTakeSnapshot() throws {
- print("\(ProcessInfo.processInfo.environment)")
guard let directory = ProcessInfo.processInfo.environment["SNAPSHOT_DIR"] else {
fatalError("Set directory to the build environment variables with key `SNAPSHOT_DIR`.")
}
From bab6ae4a63057a37871724c44771be5501f082a6 Mon Sep 17 00:00:00 2001
From: ra1028
Date: Tue, 13 Feb 2024 20:42:49 +0900
Subject: [PATCH 02/10] Set maxSize for preview image
---
.../Internal/SnapshotWindow.swift | 16 ++++++++--
.../SnapshotSupport/SnapshotSupport.swift | 31 +++++++++----------
.../Internal/Utilities/ImageLoader.swift | 1 +
3 files changed, 28 insertions(+), 20 deletions(-)
diff --git a/Sources/Playbook/SnapshotSupport/Internal/SnapshotWindow.swift b/Sources/Playbook/SnapshotSupport/Internal/SnapshotWindow.swift
index 249f967..a88ce65 100644
--- a/Sources/Playbook/SnapshotSupport/Internal/SnapshotWindow.swift
+++ b/Sources/Playbook/SnapshotSupport/Internal/SnapshotWindow.swift
@@ -4,6 +4,7 @@ internal final class SnapshotWindow: UIWindow {
private let scenario: Scenario
private let device: SnapshotDevice
private let scenarioViewController: ScenarioViewController
+ private let maxSize: CGSize?
var contentView: UIView? {
scenarioViewController.contentViewController?.view
@@ -24,7 +25,11 @@ internal final class SnapshotWindow: UIWindow {
return safeAreaInsets
}
- init(scenario: Scenario, device: SnapshotDevice) {
+ init(
+ scenario: Scenario,
+ device: SnapshotDevice,
+ maxSize: CGSize? = nil
+ ) {
let context = ScenarioContext(
snapshotWaiter: SnapshotWaiter(),
isSnapshot: true,
@@ -33,6 +38,7 @@ internal final class SnapshotWindow: UIWindow {
self.scenario = scenario
self.device = device
+ self.maxSize = maxSize
self.scenarioViewController = ScenarioViewController(context: context, scenario: scenario)
super.init(frame: .zero)
@@ -51,9 +57,13 @@ internal final class SnapshotWindow: UIWindow {
layer.speed = .greatestFiniteMagnitude
rootViewController = scenarioViewController
isHidden = false
+
+ let idealWidth = scenario.layout.fixedWidth ?? device.size.width
+ let idealHeight = scenario.layout.fixedHeight ?? device.size.height
+
frame.size = CGSize(
- width: scenario.layout.fixedWidth ?? device.size.width,
- height: scenario.layout.fixedHeight ?? device.size.height
+ width: maxSize.map { min($0.width, idealWidth) } ?? idealWidth,
+ height: maxSize.map { min($0.height, idealHeight) } ?? idealHeight
)
if window != nil {
diff --git a/Sources/Playbook/SnapshotSupport/SnapshotSupport.swift b/Sources/Playbook/SnapshotSupport/SnapshotSupport.swift
index d9d6781..801b1c0 100644
--- a/Sources/Playbook/SnapshotSupport/SnapshotSupport.swift
+++ b/Sources/Playbook/SnapshotSupport/SnapshotSupport.swift
@@ -40,6 +40,7 @@ public enum SnapshotSupport {
for scenario: Scenario,
on device: SnapshotDevice,
format: ImageFormat,
+ maxSize: CGSize? = nil,
scale: CGFloat = UIScreen.main.scale,
keyWindow: UIWindow? = nil,
viewPreprocessor: ((UIView) -> UIView)? = nil,
@@ -48,6 +49,7 @@ public enum SnapshotSupport {
makeResource(
for: scenario,
on: device,
+ maxSize: maxSize,
scale: scale,
keyWindow: keyWindow,
viewPreprocessor: viewPreprocessor
@@ -73,6 +75,7 @@ public enum SnapshotSupport {
for scenario: Scenario,
on device: SnapshotDevice,
scale: CGFloat = UIScreen.main.scale,
+ maxSize: CGSize? = nil,
keyWindow: UIWindow? = nil,
viewPreprocessor: ((UIView) -> UIView)? = nil,
handler: @escaping (UIImage) -> Void
@@ -80,6 +83,7 @@ public enum SnapshotSupport {
makeResource(
for: scenario,
on: device,
+ maxSize: maxSize,
scale: scale,
keyWindow: keyWindow,
viewPreprocessor: viewPreprocessor
@@ -98,15 +102,15 @@ private extension SnapshotSupport {
static func makeResource(
for scenario: Scenario,
on device: SnapshotDevice,
+ maxSize: CGSize?,
scale: CGFloat,
keyWindow: UIWindow?,
viewPreprocessor: ((UIView) -> UIView)? = nil,
completion: @escaping (Resource) -> Void
) {
withoutAnimation {
- let window = SnapshotWindow(scenario: scenario, device: device)
+ let window = SnapshotWindow(scenario: scenario, device: device, maxSize: maxSize)
let contentView = window.contentView!
-
let isEmbedInKeyWindow: Bool
if let keyWindow = keyWindow {
@@ -128,22 +132,15 @@ private extension SnapshotSupport {
let format = UIGraphicsImageRendererFormat(for: device.traitCollection)
format.scale = scale
+ format.preferredRange = .standard
- if #available(iOS 12.0, *) {
- format.preferredRange = .standard
- }
- else {
- format.prefersExtendedRange = false
- }
-
- var snapshotView: UIView
-
- if let viewPreprocessor = viewPreprocessor {
- snapshotView = viewPreprocessor(contentView)
- }
- else {
- snapshotView = contentView
- }
+ let snapshotView =
+ if let viewPreprocessor = viewPreprocessor {
+ viewPreprocessor(contentView)
+ }
+ else {
+ contentView
+ }
let renderer = UIGraphicsImageRenderer(bounds: snapshotView.bounds, format: format)
let actions: UIGraphicsDrawingActions = { context in
diff --git a/Sources/PlaybookUI/Internal/Utilities/ImageLoader.swift b/Sources/PlaybookUI/Internal/Utilities/ImageLoader.swift
index febe7ad..8993267 100644
--- a/Sources/PlaybookUI/Internal/Utilities/ImageLoader.swift
+++ b/Sources/PlaybookUI/Internal/Utilities/ImageLoader.swift
@@ -82,6 +82,7 @@ private extension ImageLoader {
)
),
format: .png,
+ maxSize: item.source.size,
scale: item.source.scale
) { [weak self] data in
guard let self else {
From 40109a7b29b706087b9a9c80f7b1a468e099b655 Mon Sep 17 00:00:00 2001
From: ra1028
Date: Wed, 14 Feb 2024 15:40:32 +0900
Subject: [PATCH 03/10] Further performance adjustment
---
Sources/PlaybookUI/Internal/Utilities/ImageLoader.swift | 7 +++----
Sources/PlaybookUI/Internal/Views/GalleryKindRow.swift | 2 +-
Sources/PlaybookUI/Internal/Views/GalleryThumbnail.swift | 2 +-
3 files changed, 5 insertions(+), 6 deletions(-)
diff --git a/Sources/PlaybookUI/Internal/Utilities/ImageLoader.swift b/Sources/PlaybookUI/Internal/Utilities/ImageLoader.swift
index 8993267..27e08b6 100644
--- a/Sources/PlaybookUI/Internal/Utilities/ImageLoader.swift
+++ b/Sources/PlaybookUI/Internal/Utilities/ImageLoader.swift
@@ -49,7 +49,6 @@ private extension ImageLoader {
}
guard !item.isCancelled else {
- queue.removeFirst()
return startNext()
}
@@ -62,7 +61,6 @@ private extension ImageLoader {
}
guard !item.isCancelled else {
- queue.removeFirst()
return startNext()
}
@@ -93,7 +91,6 @@ private extension ImageLoader {
item.continuation?.resume(returning: image)
item.continuation = nil
imageCache.create(file: data, for: item.source)
- queue.removeFirst()
startNext()
}
}
@@ -104,7 +101,8 @@ private extension ImageLoader {
return
}
- if sequentiallyProcessedCount < 4 {
+ if sequentiallyProcessedCount < 3 {
+ queue.removeFirst()
start()
}
else {
@@ -114,6 +112,7 @@ private extension ImageLoader {
}
sequentiallyProcessedCount = 0
+ queue.removeFirst()
start()
}
}
diff --git a/Sources/PlaybookUI/Internal/Views/GalleryKindRow.swift b/Sources/PlaybookUI/Internal/Views/GalleryKindRow.swift
index 546b556..b317c80 100644
--- a/Sources/PlaybookUI/Internal/Views/GalleryKindRow.swift
+++ b/Sources/PlaybookUI/Internal/Views/GalleryKindRow.swift
@@ -23,7 +23,7 @@ internal struct GalleryKindRow: View {
.padding(.horizontal, 24)
ScrollView(.horizontal, showsIndicators: false) {
- LazyHStack(alignment: .top, spacing: 0) {
+ LazyHStack(spacing: 8) {
ForEach(data.scenarios, id: \.scenario.name) { data in
Button {
onSelect(data)
diff --git a/Sources/PlaybookUI/Internal/Views/GalleryThumbnail.swift b/Sources/PlaybookUI/Internal/Views/GalleryThumbnail.swift
index a6ed9a2..85f6a90 100644
--- a/Sources/PlaybookUI/Internal/Views/GalleryThumbnail.swift
+++ b/Sources/PlaybookUI/Internal/Views/GalleryThumbnail.swift
@@ -45,7 +45,7 @@ internal struct GalleryThumbnail: View {
.onChange(of: colorScheme) { _ in
image = nil
}
- .task(id: colorScheme) {
+ .task(id: colorScheme, priority: .background) {
let source = ImageSource(
scenario: data.scenario,
kind: data.kind,
From 76efa4e250cdef4e72a078d34bbcaa368b3fd2ed Mon Sep 17 00:00:00 2001
From: ra1028
Date: Thu, 16 May 2024 17:25:50 +0900
Subject: [PATCH 04/10] Refactoring
---
.../Internal/Utilities/ImageCache.swift | 4 +
.../Internal/Utilities/UIColor.swift | 3 +-
.../Internal/Views/CatalogSearchPane.swift | 106 ++++++++++--------
.../Internal/Views/CatalogSplit.swift | 15 +--
.../Internal/Views/CatalogTop.swift | 2 +-
.../Internal/Views/GalleryDetail.swift | 2 +-
.../Internal/Views/GalleryThumbnail.swift | 2 +-
Sources/PlaybookUI/PlaybookGallery.swift | 27 ++---
8 files changed, 85 insertions(+), 76 deletions(-)
diff --git a/Sources/PlaybookUI/Internal/Utilities/ImageCache.swift b/Sources/PlaybookUI/Internal/Utilities/ImageCache.swift
index f2943af..3cc2b90 100644
--- a/Sources/PlaybookUI/Internal/Utilities/ImageCache.swift
+++ b/Sources/PlaybookUI/Internal/Utilities/ImageCache.swift
@@ -69,6 +69,10 @@ private extension ImageCache {
}
func remove(at url: URL) {
+ guard fileManager.fileExists(atPath: url.path) else {
+ return
+ }
+
do {
try fileManager.removeItem(at: url)
}
diff --git a/Sources/PlaybookUI/Internal/Utilities/UIColor.swift b/Sources/PlaybookUI/Internal/Utilities/UIColor.swift
index 3310ad8..3f2fce1 100644
--- a/Sources/PlaybookUI/Internal/Utilities/UIColor.swift
+++ b/Sources/PlaybookUI/Internal/Utilities/UIColor.swift
@@ -4,10 +4,9 @@ internal extension UIColor {
static let primaryBlue = UIColor(hex: 0x048DFF)
static let highlight = UIColor.systemYellow
static let translucentFill = UIColor.secondarySystemFill
- static let primaryBackground = UIColor { traitCollection in
+ static let background = UIColor { traitCollection in
traitCollection.userInterfaceStyle == .light ? .white : .black
}
- static let secondaryBackground = UIColor.secondarySystemBackground
}
private extension UIColor {
diff --git a/Sources/PlaybookUI/Internal/Views/CatalogSearchPane.swift b/Sources/PlaybookUI/Internal/Views/CatalogSearchPane.swift
index 7a1202c..8291b90 100644
--- a/Sources/PlaybookUI/Internal/Views/CatalogSearchPane.swift
+++ b/Sources/PlaybookUI/Internal/Views/CatalogSearchPane.swift
@@ -10,73 +10,81 @@ internal struct CatalogSearchPane: View {
private var isFocused
var body: some View {
- VStack(spacing: 0) {
- Spacer.fixed(length: 16)
+ HStack(spacing: 0) {
+ VStack(spacing: 0) {
+ Spacer.fixed(length: 16)
- SearchBar(text: $searchState.query)
- .focused($isFocused)
- Counter(
- count: searchState.result.count,
- total: searchState.result.total
- )
+ SearchBar(text: $searchState.query)
+ .focused($isFocused)
+ Counter(
+ count: searchState.result.count,
+ total: searchState.result.total
+ )
- Spacer.fixed(length: 16)
+ Spacer.fixed(length: 16)
- Divider()
+ Divider()
- List {
- Group {
- if searchState.result.kinds.isEmpty {
- UnavailableView(
- symbol: .magnifyingglass,
- description: "No Result for \"\(searchState.query)\""
- )
- }
- else {
- ForEach(searchState.result.kinds, id: \.kind) { data in
- let isExpanded = catalogState.currentExpandedKinds.contains(data.kind)
+ List {
+ Group {
+ if searchState.result.kinds.isEmpty {
+ UnavailableView(
+ symbol: .magnifyingglass,
+ description: "No Result for \"\(searchState.query)\""
+ )
+ }
+ else {
+ ForEach(searchState.result.kinds, id: \.kind) { data in
+ let isExpanded = catalogState.currentExpandedKinds.contains(data.kind)
- CatalogKindRow(data: data, isExpanded: isExpanded) {
- withAnimation(.smooth(duration: 0.1)) {
- if isExpanded {
- catalogState.currentExpandedKinds.remove(data.kind)
- }
- else {
- catalogState.currentExpandedKinds.insert(data.kind)
+ CatalogKindRow(data: data, isExpanded: isExpanded) {
+ withAnimation(.smooth(duration: 0.1)) {
+ if isExpanded {
+ catalogState.currentExpandedKinds.remove(data.kind)
+ }
+ else {
+ catalogState.currentExpandedKinds.insert(data.kind)
+ }
}
}
- }
- if isExpanded {
- ForEach(data.scenarios, id: \.scenario.name) { data in
- let select = SelectData(
- kind: data.kind,
- scenario: data.scenario
- )
+ if isExpanded {
+ ForEach(data.scenarios, id: \.scenario.name) { data in
+ let select = SelectData(
+ kind: data.kind,
+ scenario: data.scenario
+ )
- CatalogScenarioRow(
- data: data,
- isSelected: catalogState.selected?.id == select.id
- ) {
- catalogState.selected = select
+ CatalogScenarioRow(
+ data: data,
+ isSelected: catalogState.selected?.id == select.id
+ ) {
+ catalogState.selected = select
+ }
}
}
}
}
- }
- Spacer.fixed(length: 16)
+ Spacer.fixed(length: 16)
+ }
+ .listRowSpacing(.zero)
+ .listRowInsets(EdgeInsets())
+ .listRowSeparator(.hidden)
+ .listRowBackground(Color.clear)
}
- .listRowSpacing(.zero)
- .listRowInsets(EdgeInsets())
- .listRowSeparator(.hidden)
- .listRowBackground(Color.clear)
+ .listStyle(.plain)
+ .environment(\.defaultMinListRowHeight, 0)
}
- .listStyle(.plain)
- .environment(\.defaultMinListRowHeight, 0)
+
+ Rectangle()
+ .fill(Color(.translucentFill))
+ .frame(width: 2)
+ .fixedSize(horizontal: true, vertical: false)
+ .ignoresSafeArea()
}
.background {
- Color(.secondaryBackground)
+ Color(.background)
.ignoresSafeArea()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
diff --git a/Sources/PlaybookUI/Internal/Views/CatalogSplit.swift b/Sources/PlaybookUI/Internal/Views/CatalogSplit.swift
index 523ccac..b4d6878 100644
--- a/Sources/PlaybookUI/Internal/Views/CatalogSplit.swift
+++ b/Sources/PlaybookUI/Internal/Views/CatalogSplit.swift
@@ -11,16 +11,9 @@ internal struct CatalogSplit: View {
let sidebarWidth = geometry.size.width * 0.4
ZStack(alignment: .leading) {
- HStack(spacing: 0) {
- CatalogSearchPane()
- Divider()
- .ignoresSafeArea()
- }
- .frame(width: sidebarWidth)
- .offset(x: catalogState.isSearchPainCollapsed ? -sidebarWidth / 2 : 0)
-
HStack(spacing: 0) {
Spacer.fixed(length: catalogState.isSearchPainCollapsed ? 0 : sidebarWidth)
+
CatalogTop()
.transformEnvironment(\.horizontalSizeClass) { sizeClass in
if !catalogState.isSearchPainCollapsed {
@@ -28,8 +21,12 @@ internal struct CatalogSplit: View {
}
}
}
+
+ CatalogSearchPane()
+ .frame(width: sidebarWidth)
+ .offset(x: catalogState.isSearchPainCollapsed ? -sidebarWidth : 0)
}
}
- .animation(.smooth(duration: 0.3), value: catalogState.isSearchPainCollapsed)
+ .animation(.snappy(duration: 0.3), value: catalogState.isSearchPainCollapsed)
}
}
diff --git a/Sources/PlaybookUI/Internal/Views/CatalogTop.swift b/Sources/PlaybookUI/Internal/Views/CatalogTop.swift
index d32df1a..539b87e 100644
--- a/Sources/PlaybookUI/Internal/Views/CatalogTop.swift
+++ b/Sources/PlaybookUI/Internal/Views/CatalogTop.swift
@@ -30,7 +30,7 @@ internal struct CatalogTop: View {
}
.frame(maxHeight: .infinity)
.background {
- Color(.primaryBackground)
+ Color(.background)
.ignoresSafeArea()
}
.ignoresSafeArea()
diff --git a/Sources/PlaybookUI/Internal/Views/GalleryDetail.swift b/Sources/PlaybookUI/Internal/Views/GalleryDetail.swift
index 7cb149b..7233aa2 100644
--- a/Sources/PlaybookUI/Internal/Views/GalleryDetail.swift
+++ b/Sources/PlaybookUI/Internal/Views/GalleryDetail.swift
@@ -27,7 +27,7 @@ internal struct GalleryDetail: View {
}
}
.background {
- Color(.primaryBackground)
+ Color(.background)
.ignoresSafeArea()
}
}
diff --git a/Sources/PlaybookUI/Internal/Views/GalleryThumbnail.swift b/Sources/PlaybookUI/Internal/Views/GalleryThumbnail.swift
index 85f6a90..1b45e23 100644
--- a/Sources/PlaybookUI/Internal/Views/GalleryThumbnail.swift
+++ b/Sources/PlaybookUI/Internal/Views/GalleryThumbnail.swift
@@ -18,7 +18,7 @@ internal struct GalleryThumbnail: View {
var body: some View {
ZStack {
- Color(.primaryBackground)
+ Color(.background)
if let image {
Image(uiImage: image)
diff --git a/Sources/PlaybookUI/PlaybookGallery.swift b/Sources/PlaybookUI/PlaybookGallery.swift
index c693f9e..898bf07 100644
--- a/Sources/PlaybookUI/PlaybookGallery.swift
+++ b/Sources/PlaybookUI/PlaybookGallery.swift
@@ -55,17 +55,7 @@ public struct PlaybookGallery: View {
}
}
- Rectangle()
- .fill(.clear)
- .frame(height: 24)
- .frame(maxWidth: .infinity)
- .contextMenu {
- Button {
- galleryState.clearImageCache()
- } label: {
- Text("Clear image cache")
- }
- }
+ Spacer.fixed(length: 24)
}
.listRowSpacing(.zero)
.listRowInsets(EdgeInsets())
@@ -77,7 +67,7 @@ public struct PlaybookGallery: View {
.environment(\.defaultMinListRowHeight, 0)
.navigationTitleIfPresent(title)
.background {
- Color(.primaryBackground)
+ Color(.background)
.ignoresSafeArea()
}
.ignoresSafeArea(.keyboard)
@@ -86,7 +76,18 @@ public struct PlaybookGallery: View {
}
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
- ColorSchemePicker(colorScheme: $galleryState.colorScheme)
+ HStack {
+ Menu {
+ Button("Clear Thumbnail Cache") {
+ galleryState.clearImageCache()
+ }
+ } label: {
+ Image(symbol: .ellipsisCircle)
+ .imageStyle(font: .subheadline)
+ }
+
+ ColorSchemePicker(colorScheme: $galleryState.colorScheme)
+ }
}
}
}
From b0bc6bc6eeb7d038282fb6cbc2ce0540c2a49b3b Mon Sep 17 00:00:00 2001
From: ra1028
Date: Thu, 16 May 2024 17:55:18 +0900
Subject: [PATCH 05/10] Refactoring
---
.../PlaybookExample.xcodeproj/project.pbxproj | 24 ++++------
Example/SampleApp/AppDelegate.swift | 8 ----
Example/SampleApp/SampleApp.swift | 12 +++++
Example/SampleApp/SceneDelegate.swift | 16 -------
Example/SamplePlaybook/AppDelegate.swift | 13 ------
.../SamplePlaybook/SamplePlaybookApp.swift | 37 +++++++++++++++
Example/SamplePlaybook/SceneDelegate.swift | 45 -------------------
Example/SampleSnapshot/SnapshotTests.swift | 10 ++++-
Tests/SnapshotTests.swift | 10 ++++-
9 files changed, 75 insertions(+), 100 deletions(-)
delete mode 100644 Example/SampleApp/AppDelegate.swift
create mode 100644 Example/SampleApp/SampleApp.swift
delete mode 100644 Example/SampleApp/SceneDelegate.swift
delete mode 100644 Example/SamplePlaybook/AppDelegate.swift
create mode 100644 Example/SamplePlaybook/SamplePlaybookApp.swift
delete mode 100644 Example/SamplePlaybook/SceneDelegate.swift
diff --git a/Example/PlaybookExample.xcodeproj/project.pbxproj b/Example/PlaybookExample.xcodeproj/project.pbxproj
index 31242bb..ccd30cb 100644
--- a/Example/PlaybookExample.xcodeproj/project.pbxproj
+++ b/Example/PlaybookExample.xcodeproj/project.pbxproj
@@ -25,13 +25,10 @@
33A29BC1583DA6CE1EDD2594 /* SampleComponent.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 802EE057DE943B8FD99F7159 /* SampleComponent.framework */; };
343FF44603432D918D8010DF /* chincoteague.jpg in Resources */ = {isa = PBXBuildFile; fileRef = F5340487504EF97878690618 /* chincoteague.jpg */; };
39BA1BC86174B2510BCF468B /* LICENSE.txt in Resources */ = {isa = PBXBuildFile; fileRef = 9BCBC75F3CDA58F556FFD4F6 /* LICENSE.txt */; };
- 422B2E8BBE8F5E846E5BED22 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 205F01EE8189B8061B86AFE6 /* SceneDelegate.swift */; };
- 4A36F69B4A2D71EE48F035ED /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78CCF6C39F07F560470DB160 /* SceneDelegate.swift */; };
4C4E8DE7B7BD2D664D3EF142 /* HikeGraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D2009D3129A8AE87CC9C487 /* HikeGraph.swift */; };
53076BBA69A2F07A31DEF069 /* ProfileSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4261FFCE72EA7177DD8AAB04 /* ProfileSummary.swift */; };
5AA1FDE26E5A92BD3279285B /* PlaybookSnapshot.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = F7E213C737B9841E563D7E2A /* PlaybookSnapshot.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
6AF458958D42E48FC67E4139 /* Home.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AE02FE9D07ABA0A8E18AA5F /* Home.swift */; };
- 6E3A8440E87F5F1AA429E221 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 931EB6CF426817D6DA7DC87A /* AppDelegate.swift */; };
755C557D941D16F1A5866019 /* stmarylake.jpg in Resources */ = {isa = PBXBuildFile; fileRef = E7980DB51A1EB6129121C5D6 /* stmarylake.jpg */; };
789D10062010CF2025E7CCD9 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 17BBF319282057CB0EDF5771 /* Preview Assets.xcassets */; };
7C7DC8A8ED8B023DF230784A /* Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21C5FED0D815648EB02D6C75 /* Stubs.swift */; };
@@ -39,7 +36,6 @@
7EA3709C5E6CB95FF44B94C2 /* HikeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 484534BAAFC74B2B8E662666 /* HikeView.swift */; };
830C2CEB3646863325B61A22 /* charleyrivers.jpg in Resources */ = {isa = PBXBuildFile; fileRef = FDC78245A1D82930D4681993 /* charleyrivers.jpg */; };
8541D69B45364B394EA96EB0 /* icybay.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 7B580B6526E763D4EF39BCA3 /* icybay.jpg */; };
- 8A20BC3C4CA21E04018975F8 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B2EF24DF841DE1AF0D13E9C /* AppDelegate.swift */; };
8E2FB4D2C7A1222EDFFAB73E /* PlaybookSnapshot.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F7E213C737B9841E563D7E2A /* PlaybookSnapshot.framework */; };
8F5CC609A5E91A2A7CE37C1F /* Landmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB791DC91AC218875BEC261B /* Landmark.swift */; };
95225FB354D6B0D7F08C5B97 /* silversalmoncreek.jpg in Resources */ = {isa = PBXBuildFile; fileRef = C191D6BDDA8CC5E91BD3D2E2 /* silversalmoncreek.jpg */; };
@@ -60,7 +56,9 @@
CC3C9380122ED1D16DF026F8 /* rainbowlake.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 2496A95B98D6099F1FB173CD /* rainbowlake.jpg */; };
CF50C9E3A7EAC7EF06DC45C6 /* ProfileHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69803A7B28BE35CF63EB7D29 /* ProfileHost.swift */; };
CF9EB7737B7614C3C50C1AE3 /* GraphCapsule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EDFD424C343AC301DB0F234 /* GraphCapsule.swift */; };
+ D463E72F10A847CBFCDAB091 /* SampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7C1FC6F257946E415AEEBF5 /* SampleApp.swift */; };
D5D7843B0C4D8283D6C9FE0C /* twinlake.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 3CC78902D970A9EC960165D6 /* twinlake.jpg */; };
+ D9B02818BC60E3FA87BC3786 /* SamplePlaybookApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72EAF4F711E12ABE5A35679C /* SamplePlaybookApp.swift */; };
E09DD48E3FF59F49BFE27F9E /* PlaybookUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CA18B8BA6AA03488B078FEDF /* PlaybookUI.framework */; };
E0B46F41D197DD8A964865A2 /* LandmarkList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D1383BDBC69288B92C18310 /* LandmarkList.swift */; };
E6F911A5AE8805A9049A3D80 /* SnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54634E63D9F598E50BFEE94E /* SnapshotTests.swift */; };
@@ -182,7 +180,6 @@
/* Begin PBXFileReference section */
0143289ABDCF133AE1FE6FBF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; };
17BBF319282057CB0EDF5771 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
- 205F01EE8189B8061B86AFE6 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
21C5FED0D815648EB02D6C75 /* Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stubs.swift; sourceTree = ""; };
2496A95B98D6099F1FB173CD /* rainbowlake.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = rainbowlake.jpg; sourceTree = ""; };
2F47ACD6FB441ECE829B1C68 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; };
@@ -199,13 +196,12 @@
54634E63D9F598E50BFEE94E /* SnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnapshotTests.swift; sourceTree = ""; };
54A62FB73D841AF632775CDB /* chilkoottrail.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = chilkoottrail.jpg; sourceTree = ""; };
5AE02FE9D07ABA0A8E18AA5F /* Home.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Home.swift; sourceTree = ""; };
- 5B2EF24DF841DE1AF0D13E9C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
5D1383BDBC69288B92C18310 /* LandmarkList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandmarkList.swift; sourceTree = ""; };
5D4607676B6D47E4F88EE157 /* AllScenarios.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllScenarios.swift; sourceTree = ""; };
69803A7B28BE35CF63EB7D29 /* ProfileHost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHost.swift; sourceTree = ""; };
6B22197BF1CB4F213B1A0A74 /* LandmarkRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandmarkRow.swift; sourceTree = ""; };
6EDFD424C343AC301DB0F234 /* GraphCapsule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphCapsule.swift; sourceTree = ""; };
- 78CCF6C39F07F560470DB160 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
+ 72EAF4F711E12ABE5A35679C /* SamplePlaybookApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SamplePlaybookApp.swift; sourceTree = ""; };
7B580B6526E763D4EF39BCA3 /* icybay.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = icybay.jpg; sourceTree = ""; };
7D1DB2006C4C3195D49D9B27 /* Playbook */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Playbook; path = ../Playbook.xcodeproj; sourceTree = ""; };
7D311553C1081C70DEE41E21 /* BadgeSymbol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeSymbol.swift; sourceTree = ""; };
@@ -213,7 +209,6 @@
802EE057DE943B8FD99F7159 /* SampleComponent.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SampleComponent.framework; sourceTree = BUILT_PRODUCTS_DIR; };
86B3BE160CAA6EA63D0EE41C /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
889E33DF654D7927431CE42B /* ProfilesScenarios.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilesScenarios.swift; sourceTree = ""; };
- 931EB6CF426817D6DA7DC87A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
932B441579D6EF86C49A5568 /* hiddenlake.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = hiddenlake.jpg; sourceTree = ""; };
9BCBC75F3CDA58F556FFD4F6 /* LICENSE.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE.txt; sourceTree = ""; };
9FD58DD89E0885F4B031832B /* SupportingViewsScenarios.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportingViewsScenarios.swift; sourceTree = ""; };
@@ -225,6 +220,7 @@
B1C9731D0286CD995E40C994 /* Badge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Badge.swift; sourceTree = ""; };
B4447572897C5E2490BE2766 /* ProfileEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileEditor.swift; sourceTree = ""; };
B733775B65E98E7837F3D253 /* turtlerock.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = turtlerock.jpg; sourceTree = ""; };
+ B7C1FC6F257946E415AEEBF5 /* SampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleApp.swift; sourceTree = ""; };
BF570FCA8F1BAD787DBF2478 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; };
C09BDB1DA55B761350F6CCD6 /* hikeData.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = hikeData.json; sourceTree = ""; };
C191D6BDDA8CC5E91BD3D2E2 /* silversalmoncreek.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = silversalmoncreek.jpg; sourceTree = ""; };
@@ -360,11 +356,10 @@
5F827EEB63647EAD64D384BD /* SamplePlaybook */ = {
isa = PBXGroup;
children = (
- 931EB6CF426817D6DA7DC87A /* AppDelegate.swift */,
CD1E2464BE1A9847D81EA9E0 /* Assets.xcassets */,
2F47ACD6FB441ECE829B1C68 /* Info.plist */,
7EFDE9E4395F888AFC93E883 /* LaunchScreen.storyboard */,
- 205F01EE8189B8061B86AFE6 /* SceneDelegate.swift */,
+ 72EAF4F711E12ABE5A35679C /* SamplePlaybookApp.swift */,
21C5FED0D815648EB02D6C75 /* Stubs.swift */,
4456CE868E6FD7B688FCC0A5 /* Preview Content */,
CD062903B73FDEEE91F50B59 /* Scenarios */,
@@ -397,11 +392,10 @@
B9A55EDF9D853A1F58106480 /* SampleApp */ = {
isa = PBXGroup;
children = (
- 5B2EF24DF841DE1AF0D13E9C /* AppDelegate.swift */,
AE57203A490F0D29D50C0BF3 /* Assets.xcassets */,
BF570FCA8F1BAD787DBF2478 /* Info.plist */,
D6FCA2C4E87CF627BD99290A /* LaunchScreen.storyboard */,
- 78CCF6C39F07F560470DB160 /* SceneDelegate.swift */,
+ B7C1FC6F257946E415AEEBF5 /* SampleApp.swift */,
1E1C8AE035F9FE2E6E6CFE13 /* Preview Content */,
);
path = SampleApp;
@@ -672,10 +666,9 @@
buildActionMask = 2147483647;
files = (
ECB94EEC9015DA35DF840C85 /* AllScenarios.swift in Sources */,
- 6E3A8440E87F5F1AA429E221 /* AppDelegate.swift in Sources */,
0C1124533ABE0565B314A44F /* HomeScenarios.swift in Sources */,
2BC2750C9D283AD4EA17CC2E /* ProfilesScenarios.swift in Sources */,
- 422B2E8BBE8F5E846E5BED22 /* SceneDelegate.swift in Sources */,
+ D9B02818BC60E3FA87BC3786 /* SamplePlaybookApp.swift in Sources */,
7C7DC8A8ED8B023DF230784A /* Stubs.swift in Sources */,
ACA84D231A0FE3C398D866EA /* SupportingViewsScenarios.swift in Sources */,
);
@@ -685,8 +678,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- 8A20BC3C4CA21E04018975F8 /* AppDelegate.swift in Sources */,
- 4A36F69B4A2D71EE48F035ED /* SceneDelegate.swift in Sources */,
+ D463E72F10A847CBFCDAB091 /* SampleApp.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
diff --git a/Example/SampleApp/AppDelegate.swift b/Example/SampleApp/AppDelegate.swift
deleted file mode 100644
index 2e3eb4b..0000000
--- a/Example/SampleApp/AppDelegate.swift
+++ /dev/null
@@ -1,8 +0,0 @@
-import UIKit
-
-@UIApplicationMain
-final class AppDelegate: UIResponder, UIApplicationDelegate {
- func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
- UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
- }
-}
diff --git a/Example/SampleApp/SampleApp.swift b/Example/SampleApp/SampleApp.swift
new file mode 100644
index 0000000..9f1f08e
--- /dev/null
+++ b/Example/SampleApp/SampleApp.swift
@@ -0,0 +1,12 @@
+import SwiftUI
+import SampleComponent
+
+@main
+struct SampleApp: App {
+ var body: some Scene {
+ WindowGroup {
+ CategoryHome()
+ .environmentObject(UserData())
+ }
+ }
+}
diff --git a/Example/SampleApp/SceneDelegate.swift b/Example/SampleApp/SceneDelegate.swift
deleted file mode 100644
index 665bf3b..0000000
--- a/Example/SampleApp/SceneDelegate.swift
+++ /dev/null
@@ -1,16 +0,0 @@
-import SwiftUI
-import SampleComponent
-
-final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
- var window: UIWindow?
-
- func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
- if let windowScene = scene as? UIWindowScene {
- let window = UIWindow(windowScene: windowScene)
- let rootView = CategoryHome().environmentObject(UserData())
- window.rootViewController = UIHostingController(rootView: rootView)
- self.window = window
- window.makeKeyAndVisible()
- }
- }
-}
diff --git a/Example/SamplePlaybook/AppDelegate.swift b/Example/SamplePlaybook/AppDelegate.swift
deleted file mode 100644
index b404890..0000000
--- a/Example/SamplePlaybook/AppDelegate.swift
+++ /dev/null
@@ -1,13 +0,0 @@
-import Playbook
-import UIKit
-
-@UIApplicationMain
-final class AppDelegate: UIResponder, UIApplicationDelegate {
- func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
- return true
- }
-
- func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
- UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
- }
-}
diff --git a/Example/SamplePlaybook/SamplePlaybookApp.swift b/Example/SamplePlaybook/SamplePlaybookApp.swift
new file mode 100644
index 0000000..ed6bd2d
--- /dev/null
+++ b/Example/SamplePlaybook/SamplePlaybookApp.swift
@@ -0,0 +1,37 @@
+import PlaybookUI
+import SwiftUI
+
+@main
+struct SamplePlaybookApp: App {
+ enum Tab {
+ case catalog
+ case gallery
+ }
+
+ @State
+ var tab = Tab.gallery
+
+ init() {
+ Playbook.default.add(AllScenarios.self)
+ }
+
+ var body: some Scene {
+ WindowGroup {
+ TabView(selection: $tab) {
+ PlaybookGallery()
+ .tag(Tab.gallery)
+ .tabItem {
+ Image(systemName: "rectangle.grid.3x2")
+ Text("Gallery")
+ }
+
+ PlaybookCatalog()
+ .tag(Tab.catalog)
+ .tabItem {
+ Image(systemName: "doc.text.magnifyingglass")
+ Text("Catalog")
+ }
+ }
+ }
+ }
+}
diff --git a/Example/SamplePlaybook/SceneDelegate.swift b/Example/SamplePlaybook/SceneDelegate.swift
deleted file mode 100644
index a9b943d..0000000
--- a/Example/SamplePlaybook/SceneDelegate.swift
+++ /dev/null
@@ -1,45 +0,0 @@
-import PlaybookUI
-import SwiftUI
-
-struct PlaybookView: View {
- enum Tab {
- case catalog
- case gallery
- }
-
- @State
- var tab = Tab.gallery
-
- var body: some View {
- TabView(selection: $tab) {
- PlaybookGallery()
- .tag(Tab.gallery)
- .tabItem {
- Image(systemName: "rectangle.grid.3x2")
- Text("Gallery")
- }
-
- PlaybookCatalog()
- .tag(Tab.catalog)
- .tabItem {
- Image(systemName: "doc.text.magnifyingglass")
- Text("Catalog")
- }
- }
- }
-}
-
-final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
- var window: UIWindow?
-
- func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
- guard let windowScene = scene as? UIWindowScene else { return }
- Playbook.default.add(AllScenarios.self)
-
- let window = UIWindow(windowScene: windowScene)
- window.rootViewController = UIHostingController(rootView: PlaybookView())
-
- self.window = window
- window.makeKeyAndVisible()
- }
-}
diff --git a/Example/SampleSnapshot/SnapshotTests.swift b/Example/SampleSnapshot/SnapshotTests.swift
index 76f6c49..4187fed 100644
--- a/Example/SampleSnapshot/SnapshotTests.swift
+++ b/Example/SampleSnapshot/SnapshotTests.swift
@@ -13,9 +13,17 @@ final class SnapshotTests: XCTestCase {
clean: true,
format: .png,
scale: 1,
- keyWindow: UIApplication.shared.windows.first { $0.isKeyWindow },
+ keyWindow: getKeyWindow(),
devices: [.iPhone11Pro(.portrait)]
)
)
}
+
+ func getKeyWindow() -> UIWindow? {
+ UIApplication.shared.connectedScenes
+ .lazy
+ .compactMap { $0 as? UIWindowScene }
+ .flatMap(\.windows)
+ .first(where: \.isKeyWindow)
+ }
}
diff --git a/Tests/SnapshotTests.swift b/Tests/SnapshotTests.swift
index 1114c9b..15b75d7 100644
--- a/Tests/SnapshotTests.swift
+++ b/Tests/SnapshotTests.swift
@@ -15,7 +15,7 @@ final class SnapshotTests: XCTestCase {
clean: true,
format: .png,
scale: 1,
- keyWindow: UIApplication.shared.windows.first { $0.isKeyWindow },
+ keyWindow: getKeyWindow(),
devices: [
.iPhone11Pro(.portrait),
.iPhone11Pro(.landscape).style(.dark),
@@ -29,4 +29,12 @@ final class SnapshotTests: XCTestCase {
)
)
}
+
+ func getKeyWindow() -> UIWindow? {
+ UIApplication.shared.connectedScenes
+ .lazy
+ .compactMap { $0 as? UIWindowScene }
+ .flatMap(\.windows)
+ .first(where: \.isKeyWindow)
+ }
}
From 9b6548d220d765be65b7498e57d1c64ad753517a Mon Sep 17 00:00:00 2001
From: ra1028
Date: Thu, 16 May 2024 20:07:10 +0900
Subject: [PATCH 06/10] Fix snapshot test
---
Example/SampleSnapshot/SnapshotTests.swift | 2 +-
Playbook.xcodeproj/project.pbxproj | 8 +
.../Internal/Views/CatalogDrawer.swift | 3 -
.../Internal/Views/CatalogSearchPane.swift | 1 +
.../Views/PlaybookCatalogContent.swift | 54 ++++
.../Views/PlaybookGalleryContent.swift | 102 +++++++
Sources/PlaybookUI/PlaybookCatalog.swift | 44 +--
Sources/PlaybookUI/PlaybookGallery.swift | 90 +-----
Tests/AllScenarios.swift | 1 +
Tests/CatalogScenarios.swift | 131 ++++----
Tests/GalleryScenarios.swift | 287 +-----------------
Tests/Mocks.swift | 62 ++--
Tests/SnapshotTests.swift | 7 +-
13 files changed, 270 insertions(+), 522 deletions(-)
create mode 100644 Sources/PlaybookUI/Internal/Views/PlaybookCatalogContent.swift
create mode 100644 Sources/PlaybookUI/Internal/Views/PlaybookGalleryContent.swift
diff --git a/Example/SampleSnapshot/SnapshotTests.swift b/Example/SampleSnapshot/SnapshotTests.swift
index 4187fed..59baf38 100644
--- a/Example/SampleSnapshot/SnapshotTests.swift
+++ b/Example/SampleSnapshot/SnapshotTests.swift
@@ -14,7 +14,7 @@ final class SnapshotTests: XCTestCase {
format: .png,
scale: 1,
keyWindow: getKeyWindow(),
- devices: [.iPhone11Pro(.portrait)]
+ devices: [.iPhone15Pro(.portrait)]
)
)
}
diff --git a/Playbook.xcodeproj/project.pbxproj b/Playbook.xcodeproj/project.pbxproj
index 9fb93ad..925955f 100644
--- a/Playbook.xcodeproj/project.pbxproj
+++ b/Playbook.xcodeproj/project.pbxproj
@@ -20,6 +20,8 @@
334B859A3103A83A4EC996BF /* GalleryScenarios.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BAD3FE8D577886981218938 /* GalleryScenarios.swift */; };
341406194DDF8C7A82AB96D0 /* ScenarioContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63CD111722A3B8067BFE548 /* ScenarioContext.swift */; };
358D72B04367A43CD12978FD /* ScenarioViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77D5B1C44FC9E87FD3237459 /* ScenarioViewController.swift */; };
+ 35BFB062D9494EEADDA44B7E /* PlaybookCatalogContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE860C77A4DCDA2055ACC0C /* PlaybookCatalogContent.swift */; };
+ 3A51E61C126D173E133CCF48 /* PlaybookGalleryContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21A6A1D8849030D7DD8BE135 /* PlaybookGalleryContent.swift */; };
3D179776ADD757235527A929 /* GalleryThumbnail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80C668330D834FEE619E6735 /* GalleryThumbnail.swift */; };
4104FE671C498D67FB0134C3 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B12B3F422F9EE7600CA7B8C /* ImageCache.swift */; };
43EC47CA52837935B25B6A54 /* Playbook.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 08B7BB8752007FC3BB78EC8A /* Playbook.framework */; };
@@ -148,6 +150,7 @@
0B038E054D3A0F626DD32C93 /* PlaybookUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PlaybookUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
108C38EAC8F960E10B0FA627 /* SnapshotError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnapshotError.swift; sourceTree = ""; };
11067B4E4A9D6494867D706A /* Separator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Separator.swift; sourceTree = ""; };
+ 21A6A1D8849030D7DD8BE135 /* PlaybookGalleryContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybookGalleryContent.swift; sourceTree = ""; };
2532CFEE0AB6105AEB5B74E2 /* SelectData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectData.swift; sourceTree = ""; };
25344536B72D76889805B50D /* OrderedStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderedStorage.swift; sourceTree = ""; };
29B5E679D86934A2FB5A0856 /* UnavailableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnavailableView.swift; sourceTree = ""; };
@@ -200,6 +203,7 @@
BEC649C8437116C1FFCA36D9 /* GalleryState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryState.swift; sourceTree = ""; };
C509DD2858530167BEC5BB11 /* SearchResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResult.swift; sourceTree = ""; };
C925FCA9E427334E3128E21D /* CatalogSplit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogSplit.swift; sourceTree = ""; };
+ CDE860C77A4DCDA2055ACC0C /* PlaybookCatalogContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybookCatalogContent.swift; sourceTree = ""; };
CEB51C85B7D63CEF5DC83B60 /* ScenarioSwiftUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScenarioSwiftUITests.swift; sourceTree = ""; };
D15384CB93B122425863A487 /* CatalogScenarios.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogScenarios.swift; sourceTree = ""; };
D26212C25465F186B196A9E9 /* SearchState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchState.swift; sourceTree = ""; };
@@ -391,6 +395,8 @@
80C668330D834FEE619E6735 /* GalleryThumbnail.swift */,
BCBE2887B8D2BD7EA50CD94B /* HighlightText.swift */,
85A9B46FFAE9273D84C8A395 /* MaterialView.swift */,
+ CDE860C77A4DCDA2055ACC0C /* PlaybookCatalogContent.swift */,
+ 21A6A1D8849030D7DD8BE135 /* PlaybookGalleryContent.swift */,
B446C3A548A2DC847F032CA0 /* ScenarioContentView.swift */,
559B640B10AF07E0E4755344 /* SearchBar.swift */,
11067B4E4A9D6494867D706A /* Separator.swift */,
@@ -648,7 +654,9 @@
B44C35245CFBC0043183D67C /* ImageSource.swift in Sources */,
1F6EB9E17EC82DFFE52C8FE9 /* MaterialView.swift in Sources */,
668D80F30C0A09A5667C592A /* PlaybookCatalog.swift in Sources */,
+ 35BFB062D9494EEADDA44B7E /* PlaybookCatalogContent.swift in Sources */,
2C4D9ABC6FC9696E42BE8080 /* PlaybookGallery.swift in Sources */,
+ 3A51E61C126D173E133CCF48 /* PlaybookGalleryContent.swift in Sources */,
AE0DE7D74E721B0D254353B2 /* ScenarioContentView.swift in Sources */,
0A26157EE6E7B940067CA211 /* SearchBar.swift in Sources */,
08DB4240FAF948F16398C1C9 /* SearchResult.swift in Sources */,
diff --git a/Sources/PlaybookUI/Internal/Views/CatalogDrawer.swift b/Sources/PlaybookUI/Internal/Views/CatalogDrawer.swift
index ee815bf..21d2003 100644
--- a/Sources/PlaybookUI/Internal/Views/CatalogDrawer.swift
+++ b/Sources/PlaybookUI/Internal/Views/CatalogDrawer.swift
@@ -11,9 +11,6 @@ internal struct CatalogDrawer: View {
CatalogTop()
Drawer(isCollapsed: $catalogState.isSearchPainCollapsed)
}
- .onChange(of: catalogState.selected?.id) { _ in
- catalogState.isSearchPainCollapsed = true
- }
}
}
diff --git a/Sources/PlaybookUI/Internal/Views/CatalogSearchPane.swift b/Sources/PlaybookUI/Internal/Views/CatalogSearchPane.swift
index 8291b90..73970c6 100644
--- a/Sources/PlaybookUI/Internal/Views/CatalogSearchPane.swift
+++ b/Sources/PlaybookUI/Internal/Views/CatalogSearchPane.swift
@@ -60,6 +60,7 @@ internal struct CatalogSearchPane: View {
isSelected: catalogState.selected?.id == select.id
) {
catalogState.selected = select
+ catalogState.isSearchPainCollapsed = true
}
}
}
diff --git a/Sources/PlaybookUI/Internal/Views/PlaybookCatalogContent.swift b/Sources/PlaybookUI/Internal/Views/PlaybookCatalogContent.swift
new file mode 100644
index 0000000..041ff28
--- /dev/null
+++ b/Sources/PlaybookUI/Internal/Views/PlaybookCatalogContent.swift
@@ -0,0 +1,54 @@
+import Playbook
+import SwiftUI
+
+@available(iOS 15.0, *)
+internal struct PlaybookCatalogContent: View {
+ let title: String?
+
+ @EnvironmentObject
+ private var searchState: SearchState
+ @EnvironmentObject
+ private var catalogState: CatalogState
+ @EnvironmentObject
+ private var shareState: ShareState
+ @Environment(\.horizontalSizeClass)
+ private var horizontalSizeClass
+ @Environment(\.verticalSizeClass)
+ private var verticalSizeClass
+
+ var body: some View {
+ Group {
+ switch (horizontalSizeClass, verticalSizeClass) {
+ case (.regular, .regular):
+ CatalogSplit()
+
+ default:
+ CatalogDrawer()
+ }
+ }
+ .safeAreaInset(edge: .bottom, spacing: 0) {
+ CatalogBottomBar(
+ title: title,
+ primaryItemSymbol: primaryBarItemSymbol
+ )
+ }
+ .ignoresSafeArea(.keyboard)
+ .preferredColorScheme(catalogState.colorScheme)
+ .onAppear {
+ catalogState.selectInitial(searchResult: searchState.result)
+ }
+ }
+}
+
+@available(iOS 15.0, *)
+private extension PlaybookCatalogContent {
+ var primaryBarItemSymbol: Image.SFSymbols {
+ switch (horizontalSizeClass, verticalSizeClass) {
+ case (.regular, .regular):
+ return .sidebarLeft
+
+ default:
+ return .magnifyingglass
+ }
+ }
+}
diff --git a/Sources/PlaybookUI/Internal/Views/PlaybookGalleryContent.swift b/Sources/PlaybookUI/Internal/Views/PlaybookGalleryContent.swift
new file mode 100644
index 0000000..658e3ae
--- /dev/null
+++ b/Sources/PlaybookUI/Internal/Views/PlaybookGalleryContent.swift
@@ -0,0 +1,102 @@
+import Playbook
+import SwiftUI
+
+@available(iOS 15.0, *)
+internal struct PlaybookGalleryContent: View {
+ let title: String?
+
+ @EnvironmentObject
+ private var searchState: SearchState
+ @EnvironmentObject
+ private var galleryState: GalleryState
+ @EnvironmentObject
+ private var imageLoader: ImageLoader
+ @FocusState
+ private var isFocused
+
+ var body: some View {
+ NavigationView {
+ List {
+ Group {
+ Spacer.fixed(length: 16)
+ SearchBar(text: $searchState.query)
+ .focused($isFocused)
+
+ Counter(
+ count: searchState.result.count,
+ total: searchState.result.total
+ )
+ .onDisappear {
+ isFocused = false
+ }
+
+ if searchState.result.kinds.isEmpty {
+ UnavailableView(
+ symbol: .magnifyingglass,
+ description: "No Result for \"\(searchState.query)\""
+ )
+ }
+ else {
+ ForEach(searchState.result.kinds, id: \.kind) { data in
+ GalleryKindRow(data: data) { selected in
+ galleryState.selected = SelectData(
+ kind: selected.kind,
+ scenario: selected.scenario
+ )
+ }
+ }
+ }
+
+ Spacer.fixed(length: 24)
+ }
+ .listRowSpacing(.zero)
+ .listRowInsets(EdgeInsets())
+ .listRowSeparator(.hidden)
+ .listRowBackground(Color.clear)
+ .transition(.identity)
+ }
+ .listStyle(.plain)
+ .environment(\.defaultMinListRowHeight, 0)
+ .navigationTitleIfPresent(title)
+ .background {
+ Color(.background)
+ .ignoresSafeArea()
+ }
+ .ignoresSafeArea(.keyboard)
+ .sheet(item: $galleryState.selected) { data in
+ GalleryDetail(data: data)
+ }
+ .toolbar {
+ ToolbarItem(placement: .topBarTrailing) {
+ HStack {
+ Menu {
+ Button("Clear Thumbnail Cache") {
+ galleryState.clearImageCache()
+ }
+ } label: {
+ Image(symbol: .ellipsisCircle)
+ .imageStyle(font: .subheadline)
+ }
+
+ ColorSchemePicker(colorScheme: $galleryState.colorScheme)
+ }
+ }
+ }
+ }
+ .navigationViewStyle(.stack)
+ .preferredColorScheme(galleryState.colorScheme)
+ }
+}
+
+@available(iOS 14.0, *)
+private extension View {
+ @ViewBuilder
+ func navigationTitleIfPresent(_ title: S?) -> some View {
+ if let title {
+ navigationTitle(title)
+ }
+ else {
+ self
+ }
+ }
+}
diff --git a/Sources/PlaybookUI/PlaybookCatalog.swift b/Sources/PlaybookUI/PlaybookCatalog.swift
index 995b9f6..b2e10fb 100644
--- a/Sources/PlaybookUI/PlaybookCatalog.swift
+++ b/Sources/PlaybookUI/PlaybookCatalog.swift
@@ -11,10 +11,6 @@ public struct PlaybookCatalog: View {
private var catalogState = CatalogState()
@StateObject
private var shareState = ShareState()
- @Environment(\.horizontalSizeClass)
- private var horizontalSizeClass
- @Environment(\.verticalSizeClass)
- private var verticalSizeClass
public init(
title: String? = nil,
@@ -25,41 +21,9 @@ public struct PlaybookCatalog: View {
}
public var body: some View {
- Group {
- switch (horizontalSizeClass, verticalSizeClass) {
- case (.regular, .regular):
- CatalogSplit()
-
- default:
- CatalogDrawer()
- }
- }
- .safeAreaInset(edge: .bottom, spacing: 0) {
- CatalogBottomBar(
- title: title,
- primaryItemSymbol: primaryBarItemSymbol
- )
- }
- .ignoresSafeArea(.keyboard)
- .preferredColorScheme(catalogState.colorScheme)
- .environmentObject(searchState)
- .environmentObject(catalogState)
- .environmentObject(shareState)
- .onAppear {
- catalogState.selectInitial(searchResult: searchState.result)
- }
- }
-}
-
-@available(iOS 15.0, *)
-private extension PlaybookCatalog {
- var primaryBarItemSymbol: Image.SFSymbols {
- switch (horizontalSizeClass, verticalSizeClass) {
- case (.regular, .regular):
- return .sidebarLeft
-
- default:
- return .magnifyingglass
- }
+ PlaybookCatalogContent(title: title)
+ .environmentObject(searchState)
+ .environmentObject(catalogState)
+ .environmentObject(shareState)
}
}
diff --git a/Sources/PlaybookUI/PlaybookGallery.swift b/Sources/PlaybookUI/PlaybookGallery.swift
index 898bf07..d85b192 100644
--- a/Sources/PlaybookUI/PlaybookGallery.swift
+++ b/Sources/PlaybookUI/PlaybookGallery.swift
@@ -11,8 +11,6 @@ public struct PlaybookGallery: View {
private var galleryState = GalleryState()
@StateObject
private var imageLoader = ImageLoader()
- @FocusState
- private var isFocused
public init(
title: String? = nil,
@@ -23,89 +21,9 @@ public struct PlaybookGallery: View {
}
public var body: some View {
- NavigationView {
- List {
- Group {
- Spacer.fixed(length: 16)
- SearchBar(text: $searchState.query)
- .focused($isFocused)
-
- Counter(
- count: searchState.result.count,
- total: searchState.result.total
- )
- .onDisappear {
- isFocused = false
- }
-
- if searchState.result.kinds.isEmpty {
- UnavailableView(
- symbol: .magnifyingglass,
- description: "No Result for \"\(searchState.query)\""
- )
- }
- else {
- ForEach(searchState.result.kinds, id: \.kind) { data in
- GalleryKindRow(data: data) { selected in
- galleryState.selected = SelectData(
- kind: selected.kind,
- scenario: selected.scenario
- )
- }
- }
- }
-
- Spacer.fixed(length: 24)
- }
- .listRowSpacing(.zero)
- .listRowInsets(EdgeInsets())
- .listRowSeparator(.hidden)
- .listRowBackground(Color.clear)
- .transition(.identity)
- }
- .listStyle(.plain)
- .environment(\.defaultMinListRowHeight, 0)
- .navigationTitleIfPresent(title)
- .background {
- Color(.background)
- .ignoresSafeArea()
- }
- .ignoresSafeArea(.keyboard)
- .sheet(item: $galleryState.selected) { data in
- GalleryDetail(data: data)
- }
- .toolbar {
- ToolbarItem(placement: .topBarTrailing) {
- HStack {
- Menu {
- Button("Clear Thumbnail Cache") {
- galleryState.clearImageCache()
- }
- } label: {
- Image(symbol: .ellipsisCircle)
- .imageStyle(font: .subheadline)
- }
-
- ColorSchemePicker(colorScheme: $galleryState.colorScheme)
- }
- }
- }
- }
- .navigationViewStyle(.stack)
- .preferredColorScheme(galleryState.colorScheme)
- .environmentObject(imageLoader)
- }
-}
-
-@available(iOS 14.0, *)
-private extension View {
- @ViewBuilder
- func navigationTitleIfPresent(_ title: S?) -> some View {
- if let title {
- navigationTitle(title)
- }
- else {
- self
- }
+ PlaybookGalleryContent(title: title)
+ .environmentObject(searchState)
+ .environmentObject(galleryState)
+ .environmentObject(imageLoader)
}
}
diff --git a/Tests/AllScenarios.swift b/Tests/AllScenarios.swift
index 2621ade..35e4538 100644
--- a/Tests/AllScenarios.swift
+++ b/Tests/AllScenarios.swift
@@ -1,5 +1,6 @@
import PlaybookUI
+@available(iOS 15.0, *)
struct AllScenarios: ScenarioProvider {
static func addScenarios(into playbook: Playbook) {
playbook
diff --git a/Tests/CatalogScenarios.swift b/Tests/CatalogScenarios.swift
index e38b5e3..2ca25d7 100644
--- a/Tests/CatalogScenarios.swift
+++ b/Tests/CatalogScenarios.swift
@@ -2,103 +2,78 @@ import SwiftUI
@testable import PlaybookUI
+@available(iOS 15.0, *)
enum CatalogScenarios: ScenarioProvider {
+ @MainActor
static func addScenarios(into playbook: Playbook) {
playbook.addScenarios(of: "Catalog") {
Scenario("Drawer close", layout: .fill) {
- PlaybookCatalogInternal(
- name: "TEST",
- playbook: .test,
- store: CatalogStore(
- playbook: .test,
- selectedScenario: .stub,
- isSearchTreeHidden: true
- )
- .start()
- )
+ let catalogState = CatalogState()
+ catalogState.isSearchPainCollapsed = true
+
+ return PlaybookCatalogContent(title: nil)
+ .environmentObject(SearchState(playbook: .test))
+ .environmentObject(catalogState)
+ .environmentObject(ShareState())
}
Scenario("Drawer open", layout: .fill) {
- PlaybookCatalogInternal(
- name: "TEST",
- playbook: .test,
- store: CatalogStore(
- playbook: .test,
- selectedScenario: .stub,
- openedKinds: ["Kind 1"],
- isSearchTreeHidden: false
- )
- .start()
- )
+ let catalogState = CatalogState()
+ catalogState.isSearchPainCollapsed = false
+
+ return PlaybookCatalogContent(title: nil)
+ .environmentObject(SearchState(playbook: .test))
+ .environmentObject(catalogState)
+ .environmentObject(ShareState())
}
Scenario("Drawer close empty", layout: .fill) {
- PlaybookCatalogInternal(
- name: "TEST",
- playbook: Playbook(),
- store: CatalogStore(
- playbook: Playbook(),
- isSearchTreeHidden: true
- )
- )
+ let searchState = SearchState(playbook: Playbook())
+ let catalogState = CatalogState()
+ catalogState.isSearchPainCollapsed = true
+
+ return PlaybookCatalogContent(title: nil)
+ .environmentObject(searchState)
+ .environmentObject(catalogState)
+ .environmentObject(ShareState())
}
Scenario("Drawer open empty", layout: .fill) {
- PlaybookCatalogInternal(
- name: "TEST",
- playbook: Playbook(),
- store: CatalogStore(
- playbook: Playbook(),
- isSearchTreeHidden: false
- )
- )
- }
+ let searchState = SearchState(playbook: Playbook())
+ let catalogState = CatalogState()
+ catalogState.isSearchPainCollapsed = false
- Scenario("Searching", layout: .fill) {
- PlaybookCatalogInternal(
- name: "TEST",
- playbook: .test,
- store: CatalogStore(
- playbook: .test,
- selectedScenario: .stub,
- openedSearchingKinds: Set(Playbook.test.stores.map { $0.kind }),
- isSearchTreeHidden: false
- )
- .start(with: "2")
- )
+ return PlaybookCatalogContent(title: nil)
+ .environmentObject(searchState)
+ .environmentObject(catalogState)
+ .environmentObject(ShareState())
}
- Scenario("Drawer", layout: .sizing(h: .fixed(300), v: .fill)) {
- ScenarioSearchTreeIOS13()
- .environmentObject(
- CatalogStore(
- playbook: .test,
- selectedScenario: .stub,
- openedKinds: ["Kind 1"],
- isSearchTreeHidden: false
- )
- .start()
- )
+ Scenario("Drawer open searching", layout: .fill) {
+ let searchState = SearchState(playbook: .test)
+ let catalogState = CatalogState()
+ searchState.query = "1"
+ catalogState.isSearchPainCollapsed = false
+
+ return PlaybookCatalogContent(title: nil)
+ .environmentObject(searchState)
+ .environmentObject(catalogState)
+ .environmentObject(ShareState())
}
- }
- #if swift(>=5.3)
- if #available(iOS 14.0, *) {
- playbook.addScenarios(of: "Catalog") {
- Scenario("Drawer iOS14", layout: .sizing(h: .fixed(300), v: .fill)) {
- ScenarioSearchTreeIOS14()
- .environmentObject(
- CatalogStore(
- playbook: .test,
- selectedScenario: .stub,
- openedKinds: ["Kind 1"],
- isSearchTreeHidden: false
- )
- .start()
- )
- }
+ Scenario("SearchPane", layout: .sizing(h: .fixed(300), v: .fill)) {
+ let catalogState = CatalogState()
+ catalogState.selected = SelectData(
+ kind: "Kind 1",
+ scenario: .stub("Scenario 1")
+ )
+ catalogState.expandedKinds = ["Kind 1"]
+ catalogState.isSearchPainCollapsed = false
+
+ return CatalogSearchPane()
+ .environmentObject(SearchState(playbook: .test))
+ .environmentObject(catalogState)
}
}
- #endif
}
}
diff --git a/Tests/GalleryScenarios.swift b/Tests/GalleryScenarios.swift
index 9e1aa27..e8beb96 100644
--- a/Tests/GalleryScenarios.swift
+++ b/Tests/GalleryScenarios.swift
@@ -2,292 +2,37 @@ import SwiftUI
@testable import PlaybookUI
+@available(iOS 15.0, *)
enum GalleryScenarios: ScenarioProvider {
+ @MainActor
static func addScenarios(into playbook: Playbook) {
playbook.addScenarios(of: "Gallery") {
- Scenario("Ready", layout: .fill) { context in
- PlaybookGalleryIOS13(
- name: "TEST",
- snapshotColorScheme: .light,
- store: GalleryStore(
- playbook: .test,
- preSnapshotCountLimit: 100,
- screenSize: context.screenSize.portrait,
- userInterfaceStyle: .light,
- status: .ready
- )
- .takeSnapshots()
- .start()
- )
- .environment(
- \.galleryDependency,
- GalleryDependency(
- scheduler: SchedulerMock(),
- context: context
- )
- )
- }
-
- Scenario("Preparing", layout: .fill) { context in
- PlaybookGalleryIOS13(
- name: "TEST",
- snapshotColorScheme: .light,
- store: GalleryStore(
- playbook: .test,
- preSnapshotCountLimit: 0,
- screenSize: context.screenSize.portrait,
- userInterfaceStyle: .light
- )
- .start()
- )
+ Scenario("Thumbnails", layout: .fill) { context in
+ PlaybookGallery(playbook: .test)
}
Scenario("Empty", layout: .fill) { context in
- PlaybookGalleryIOS13(
- name: "TEST",
- snapshotColorScheme: .light,
- store: GalleryStore(
- playbook: Playbook(),
- preSnapshotCountLimit: 0,
- screenSize: context.screenSize.portrait,
- userInterfaceStyle: .light
- )
- )
- .environment(
- \.galleryDependency,
- GalleryDependency(
- scheduler: SchedulerMock(),
- context: context
- )
- )
+ PlaybookGallery(playbook: Playbook())
}
Scenario("Searching", layout: .fill) { context in
- PlaybookGalleryIOS13(
- name: "TEST",
- snapshotColorScheme: .light,
- store: GalleryStore(
- playbook: .test,
- preSnapshotCountLimit: 100,
- screenSize: context.screenSize.portrait,
- userInterfaceStyle: .light,
- status: .ready
- )
- .takeSnapshots()
- .start(with: "2")
- )
- .environment(
- \.galleryDependency,
- GalleryDependency(
- scheduler: SchedulerMock(),
- context: context
- )
- )
- }
-
- Scenario("Sheet", layout: .fill) { context in
- ScenarioDisplaySheet(data: .stub, onClose: {})
- .environmentObject(
- GalleryStore(
- playbook: .test,
- preSnapshotCountLimit: 0,
- screenSize: context.screenSize.portrait,
- userInterfaceStyle: .light
- )
- )
- }
-
- Scenario("Display list", layout: .fillH) { context in
- ScenarioDisplayList(
- data: SearchedListData(
- kind: "Long Kind Long Kind Long Kind Long Kind",
- shouldHighlight: true,
- scenarios: [.stub, .stub, .stub]
- ),
- safeAreaInsets: EdgeInsets(),
- serialDispatcher: SerialMainDispatcher(interval: 0, scheduler: SchedulerMock()),
- onSelect: { _ in }
- )
- .environmentObject(
- GalleryStore(
- playbook: .test,
- preSnapshotCountLimit: 100,
- screenSize: context.screenSize.portrait,
- userInterfaceStyle: .light
- )
- .takeSnapshots()
- )
- .environment(
- \.galleryDependency,
- GalleryDependency(
- scheduler: SchedulerMock(),
- context: context
- )
- )
- }
-
- Scenario("Display empty", layout: .compressed) { context in
- ScenarioDisplay(
- store: ScenarioDisplayStore(
- data: SearchedData(
- scenario: .stub("Long Name Long Name Long Name"),
- kind: "Kind",
- shouldHighlight: true
- ),
- snapshotLoader: SnapshotLoaderMock(
- device: SnapshotDevice(name: "TEST", size: context.screenSize.portrait),
- loadImageResult: .success(nil)
- ),
- serialDispatcher: SerialMainDispatcher(interval: 0, scheduler: SchedulerMock()),
- scheduler: SchedulerMock()
- )
- )
- }
+ let searchState = SearchState(playbook: .test)
+ searchState.query = "1"
- Scenario("Display failure", layout: .compressed) { context in
- ScenarioDisplay(
- store: ScenarioDisplayStore(
- data: .stub,
- snapshotLoader: SnapshotLoaderMock(
- device: SnapshotDevice(name: "TEST", size: context.screenSize.portrait),
- loadImageResult: .failure(TestError())
- ),
- serialDispatcher: SerialMainDispatcher(interval: 0, scheduler: SchedulerMock()),
- scheduler: SchedulerMock()
- )
- )
+ return PlaybookGalleryContent(title: nil)
+ .environmentObject(searchState)
+ .environmentObject(GalleryState())
+ .environmentObject(ImageLoader())
}
- Scenario("Display failure dark", layout: .compressed) { context in
- ScenarioDisplay(
- store: ScenarioDisplayStore(
- data: .stub,
- snapshotLoader: SnapshotLoaderMock(
- device: SnapshotDevice(name: "TEST", size: context.screenSize.portrait),
- loadImageResult: .failure(TestError())
- ),
- serialDispatcher: SerialMainDispatcher(interval: 0, scheduler: SchedulerMock()),
- scheduler: SchedulerMock()
- )
- )
+ Scenario("Row", layout: .fillH) { context in
+ GalleryKindRow(data: .stub()) { _ in }
+ .environmentObject(ImageLoader())
}
- }
-
- #if swift(>=5.3)
- if #available(iOS 14.0, *) {
- playbook.addScenarios(of: "Gallery") {
- Scenario("Ready iOS14", layout: .fill) { context in
- PlaybookGalleryIOS14(
- name: "TEST",
- snapshotColorScheme: .light,
- store: GalleryStore(
- playbook: .test,
- preSnapshotCountLimit: 100,
- screenSize: context.screenSize.portrait,
- userInterfaceStyle: .light,
- status: .ready
- )
- .takeSnapshots()
- .start()
- )
- .environment(
- \.galleryDependency,
- GalleryDependency(
- scheduler: SchedulerMock(),
- context: context
- )
- )
- }
-
- Scenario("Preparing iOS14", layout: .fill) { context in
- PlaybookGalleryIOS14(
- name: "TEST",
- snapshotColorScheme: .light,
- store: GalleryStore(
- playbook: .test,
- preSnapshotCountLimit: 0,
- screenSize: context.screenSize.portrait,
- userInterfaceStyle: .light
- )
- .start()
- )
- }
-
- Scenario("Empty iOS14", layout: .fill) { context in
- PlaybookGalleryIOS14(
- name: "TEST",
- snapshotColorScheme: .light,
- store: GalleryStore(
- playbook: Playbook(),
- preSnapshotCountLimit: 0,
- screenSize: context.screenSize.portrait,
- userInterfaceStyle: .light
- )
- )
- .environment(
- \.galleryDependency,
- GalleryDependency(
- scheduler: SchedulerMock(),
- context: context
- )
- )
- }
-
- Scenario("Searching iOS14", layout: .fill) { context in
- PlaybookGalleryIOS14(
- name: "TEST",
- snapshotColorScheme: .light,
- store: GalleryStore(
- playbook: .test,
- preSnapshotCountLimit: 100,
- screenSize: context.screenSize.portrait,
- userInterfaceStyle: .light,
- status: .ready
- )
- .takeSnapshots()
- .start(with: "2")
- )
- .environment(
- \.galleryDependency,
- GalleryDependency(
- scheduler: SchedulerMock(),
- context: context
- )
- )
- }
- Scenario("Dark snapshots", layout: .fill) { context in
- PlaybookGalleryIOS14(
- name: "TEST",
- snapshotColorScheme: .dark,
- store: GalleryStore(
- playbook: .test,
- preSnapshotCountLimit: 100,
- screenSize: context.screenSize.portrait,
- userInterfaceStyle: .dark,
- status: .ready
- )
- .takeSnapshots()
- .start()
- )
- .environment(
- \.galleryDependency,
- GalleryDependency(
- scheduler: SchedulerMock(),
- context: context
- )
- )
- }
+ Scenario("Detail", layout: .fill) { context in
+ GalleryDetail(data: .stub())
}
}
- #endif
- }
-}
-
-private struct TestError: Error {}
-
-private extension CGSize {
- var portrait: CGSize {
- CGSize(width: min(width, height), height: max(width, height))
}
}
diff --git a/Tests/Mocks.swift b/Tests/Mocks.swift
index d42a9d2..8f2b8dc 100644
--- a/Tests/Mocks.swift
+++ b/Tests/Mocks.swift
@@ -3,42 +3,6 @@ import SwiftUI
@testable import PlaybookUI
-struct SchedulerMock: SchedulerProtocol {
- func schedule(on queue: DispatchQueue, action: @escaping () -> Void) {
- action()
- }
-
- func schedule(on: DispatchQueue, after interval: TimeInterval, action: @escaping () -> Void) {
- action()
- }
-}
-
-final class SnapshotLoaderMock: SnapshotLoaderProtocol {
- let device: SnapshotDevice
- let takeSnapshotResult: Data
- let loadImageResult: Result
-
- init(
- device: SnapshotDevice,
- takeSnapshotResult: Data = Data(),
- loadImageResult: Result = .success(nil)
- ) {
- self.device = device
- self.takeSnapshotResult = takeSnapshotResult
- self.loadImageResult = loadImageResult
- }
-
- func takeSnapshot(for scenario: Scenario, kind: ScenarioKind, completion: ((Data) -> Void)?) {
- completion?(takeSnapshotResult)
- }
-
- func loadImage(kind: ScenarioKind, name: ScenarioName) -> Result {
- loadImageResult
- }
-
- func clean() {}
-}
-
extension Playbook {
static let test: Playbook = {
let playbok = Playbook()
@@ -55,12 +19,30 @@ extension Playbook {
}()
}
+extension SelectData {
+ static func stub() -> Self {
+ SelectData(kind: "Kind", scenario: .stub("Scenario"))
+ }
+}
+
extension SearchedData {
- static var stub: Self {
+ static func stub(_ index: Int) -> Self {
SearchedData(
- scenario: .stub("Scenario 1"),
- kind: "Kind 1",
- shouldHighlight: false
+ kind: "Kind",
+ scenario: .stub("Scenario \(index)"),
+ highlightRange: nil
+ )
+ }
+}
+
+extension SearchedKindData {
+ static func stub(
+ scenarios: [SearchedData] = (0..<3).map { .stub($0) }
+ ) -> Self {
+ SearchedKindData(
+ kind: "Kind",
+ highlightRange: nil,
+ scenarios: scenarios
)
}
}
diff --git a/Tests/SnapshotTests.swift b/Tests/SnapshotTests.swift
index 15b75d7..7bd5f50 100644
--- a/Tests/SnapshotTests.swift
+++ b/Tests/SnapshotTests.swift
@@ -1,6 +1,7 @@
import PlaybookSnapshot
import XCTest
+@available(iOS 15.0, *)
final class SnapshotTests: XCTestCase {
func testTakeSnapshot() throws {
guard let directory = ProcessInfo.processInfo.environment["SNAPSHOT_DIR"] else {
@@ -17,9 +18,9 @@ final class SnapshotTests: XCTestCase {
scale: 1,
keyWindow: getKeyWindow(),
devices: [
- .iPhone11Pro(.portrait),
- .iPhone11Pro(.landscape).style(.dark),
- .iPhoneSE(.portrait).style(.dark),
+ .iPhone15Pro(.portrait),
+ .iPhone15Pro(.landscape).style(.dark),
+ .iPhone13Mini(.portrait),
.iPadPro12_9(.landscape),
],
viewPreprocessor: { view in
From b276b6ada67a410e1f9dd1a6161e9c5f6c6edbeb Mon Sep 17 00:00:00 2001
From: ra1028
Date: Thu, 16 May 2024 20:31:19 +0900
Subject: [PATCH 07/10] Refactoring
---
.../Internal/Views/CatalogDrawer.swift | 30 ++++++++++---------
.../Internal/Views/CatalogSearchPane.swift | 5 ++--
.../Internal/Views/CatalogSplit.swift | 8 +++--
Tests/CatalogScenarios.swift | 2 +-
4 files changed, 25 insertions(+), 20 deletions(-)
diff --git a/Sources/PlaybookUI/Internal/Views/CatalogDrawer.swift b/Sources/PlaybookUI/Internal/Views/CatalogDrawer.swift
index 21d2003..b7449bf 100644
--- a/Sources/PlaybookUI/Internal/Views/CatalogDrawer.swift
+++ b/Sources/PlaybookUI/Internal/Views/CatalogDrawer.swift
@@ -3,23 +3,22 @@ import SwiftUI
@available(iOS 15.0, *)
internal struct CatalogDrawer: View {
- @EnvironmentObject
- private var catalogState: CatalogState
-
var body: some View {
ZStack {
CatalogTop()
- Drawer(isCollapsed: $catalogState.isSearchPainCollapsed)
+ Drawer()
}
}
}
@available(iOS 15.0, *)
private struct Drawer: View {
- @Binding
- var isCollapsed: Bool
+ @EnvironmentObject
+ private var catalogState: CatalogState
var body: some View {
+ let isCollapsed = catalogState.isSearchPainCollapsed
+
GeometryReader { geometry in
let drawerWidth =
geometry.safeAreaInsets.leading
@@ -33,17 +32,20 @@ private struct Drawer: View {
.opacity(isCollapsed ? 0 : 0.3)
.ignoresSafeArea()
.onTapGesture {
- isCollapsed = true
+ catalogState.isSearchPainCollapsed = true
}
HStack(spacing: 0) {
- CatalogSearchPane()
- .frame(width: drawerWidth)
- .background {
- Rectangle()
- .ignoresSafeArea()
- .shadow(radius: 10)
- }
+ CatalogSearchPane { data in
+ catalogState.selected = data
+ catalogState.isSearchPainCollapsed = true
+ }
+ .frame(width: drawerWidth)
+ .background {
+ Rectangle()
+ .ignoresSafeArea()
+ .shadow(radius: 10)
+ }
Spacer.zero
}
diff --git a/Sources/PlaybookUI/Internal/Views/CatalogSearchPane.swift b/Sources/PlaybookUI/Internal/Views/CatalogSearchPane.swift
index 73970c6..f296d72 100644
--- a/Sources/PlaybookUI/Internal/Views/CatalogSearchPane.swift
+++ b/Sources/PlaybookUI/Internal/Views/CatalogSearchPane.swift
@@ -9,6 +9,8 @@ internal struct CatalogSearchPane: View {
@FocusState
private var isFocused
+ let onSelect: (SelectData) -> Void
+
var body: some View {
HStack(spacing: 0) {
VStack(spacing: 0) {
@@ -59,8 +61,7 @@ internal struct CatalogSearchPane: View {
data: data,
isSelected: catalogState.selected?.id == select.id
) {
- catalogState.selected = select
- catalogState.isSearchPainCollapsed = true
+ onSelect(select)
}
}
}
diff --git a/Sources/PlaybookUI/Internal/Views/CatalogSplit.swift b/Sources/PlaybookUI/Internal/Views/CatalogSplit.swift
index b4d6878..34ee3cb 100644
--- a/Sources/PlaybookUI/Internal/Views/CatalogSplit.swift
+++ b/Sources/PlaybookUI/Internal/Views/CatalogSplit.swift
@@ -22,9 +22,11 @@ internal struct CatalogSplit: View {
}
}
- CatalogSearchPane()
- .frame(width: sidebarWidth)
- .offset(x: catalogState.isSearchPainCollapsed ? -sidebarWidth : 0)
+ CatalogSearchPane { data in
+ catalogState.selected = data
+ }
+ .frame(width: sidebarWidth)
+ .offset(x: catalogState.isSearchPainCollapsed ? -sidebarWidth : 0)
}
}
.animation(.snappy(duration: 0.3), value: catalogState.isSearchPainCollapsed)
diff --git a/Tests/CatalogScenarios.swift b/Tests/CatalogScenarios.swift
index 2ca25d7..601d121 100644
--- a/Tests/CatalogScenarios.swift
+++ b/Tests/CatalogScenarios.swift
@@ -70,7 +70,7 @@ enum CatalogScenarios: ScenarioProvider {
catalogState.expandedKinds = ["Kind 1"]
catalogState.isSearchPainCollapsed = false
- return CatalogSearchPane()
+ return CatalogSearchPane { _ in }
.environmentObject(SearchState(playbook: .test))
.environmentObject(catalogState)
}
From b07f0890f077aca500ae1a92b01c1340c7343bae Mon Sep 17 00:00:00 2001
From: ra1028
Date: Mon, 20 May 2024 18:12:45 +0900
Subject: [PATCH 08/10] Update README
---
Makefile | 9 -------
README.md | 23 +++++++++---------
...napshot.png => accessibility_snapshot.png} | Bin
assets/catalog-dark.png | Bin 498154 -> 0 bytes
assets/catalog-drawer-dark.png | Bin 163797 -> 0 bytes
assets/catalog-drawer-light.png | Bin 166698 -> 0 bytes
assets/catalog-light.png | Bin 474635 -> 0 bytes
assets/catalog_dark.png | Bin 0 -> 69235 bytes
assets/catalog_detail_dark.png | Bin 0 -> 201403 bytes
assets/catalog_detail_light.png | Bin 0 -> 202997 bytes
assets/catalog_light.png | Bin 0 -> 69263 bytes
assets/demo.gif | Bin 0 -> 3382934 bytes
assets/demo.png | Bin 180799 -> 0 bytes
assets/gallery-content-dark.png | Bin 460042 -> 0 bytes
assets/gallery-content-light.png | Bin 444704 -> 0 bytes
assets/gallery-dark.png | Bin 187349 -> 0 bytes
assets/gallery-light.png | Bin 198307 -> 0 bytes
assets/gallery_dark.png | Bin 0 -> 96916 bytes
assets/gallery_detail_dark.png | Bin 0 -> 195742 bytes
assets/gallery_detail_light.png | Bin 0 -> 197296 bytes
assets/gallery_light.png | Bin 0 -> 97765 bytes
...erated-images.png => generated_images.png} | Bin
assets/mockup.gif | Bin 4180776 -> 0 bytes
assets/overview.png | Bin 0 -> 426682 bytes
assets/{reg-report.png => reg_report.png} | Bin
25 files changed, 11 insertions(+), 21 deletions(-)
rename assets/{accessibility-snapshot.png => accessibility_snapshot.png} (100%)
delete mode 100644 assets/catalog-dark.png
delete mode 100644 assets/catalog-drawer-dark.png
delete mode 100644 assets/catalog-drawer-light.png
delete mode 100644 assets/catalog-light.png
create mode 100644 assets/catalog_dark.png
create mode 100644 assets/catalog_detail_dark.png
create mode 100644 assets/catalog_detail_light.png
create mode 100644 assets/catalog_light.png
create mode 100644 assets/demo.gif
delete mode 100644 assets/demo.png
delete mode 100644 assets/gallery-content-dark.png
delete mode 100644 assets/gallery-content-light.png
delete mode 100644 assets/gallery-dark.png
delete mode 100644 assets/gallery-light.png
create mode 100644 assets/gallery_dark.png
create mode 100644 assets/gallery_detail_dark.png
create mode 100644 assets/gallery_detail_light.png
create mode 100644 assets/gallery_light.png
rename assets/{generated-images.png => generated_images.png} (100%)
delete mode 100644 assets/mockup.gif
create mode 100644 assets/overview.png
rename assets/{reg-report.png => reg_report.png} (100%)
diff --git a/Makefile b/Makefile
index 506030b..f574416 100644
--- a/Makefile
+++ b/Makefile
@@ -28,15 +28,6 @@ docs:
npm:
npm i
-.PHONY: fix-readme-links
-fix-readme-links:
- sed -i '' -E '/.?http/!s#(A library for isolated developing UI components and automatically taking snapshots of them.
-
+
# Playbook
-
+
@@ -110,7 +110,7 @@ Those that are displayed on the top screen are not actually doing layout, but ra
| Browser | Detail |
| ------- | ------ |
-|||
+|||
#### PlaybookCatalog
@@ -119,7 +119,7 @@ If you have too many scenarios, this may be more efficient than `PlaybookCatalog
| Browser | Detail |
| ------- | ------ |
-|||
+|||
#### How to Save Snapshot Images
@@ -143,15 +143,14 @@ final class SnapshotTests: XCTestCase {
directory: URL(fileURLWithPath: directory),
clean: true,
format: .png,
- keyWindow: UIApplication.shared.windows.first { $0.isKeyWindow },
- devices: [.iPhone11Pro(.portrait)]
+ devices: [.iPhone15Pro(.portrait)]
)
)
}
}
```
-
+
---
@@ -159,7 +158,7 @@ final class SnapshotTests: XCTestCase {
An extension to `Playbook` that uses [AccessibilitySnapshot](https://github.com/cashapp/AccessibilitySnapshot) to produce snapshots with accessibility information such as activation points and labels.
-
+
---
@@ -169,19 +168,19 @@ The generated snapshot images can be used for more advanced visual regression te
#### [percy](https://percy.io)
-
+
#### [reg-viz/reg-suit](https://github.com/reg-viz/reg-suit)
-
+
---
## Requirements
- Swift 5.9+
-- Xcode 15.0+
-- iOS 13.0+
+- Xcode 15.2+
+- iOS 13.0+ (PlaybookUI: iOS 15.0+)
---
diff --git a/assets/accessibility-snapshot.png b/assets/accessibility_snapshot.png
similarity index 100%
rename from assets/accessibility-snapshot.png
rename to assets/accessibility_snapshot.png
diff --git a/assets/catalog-dark.png b/assets/catalog-dark.png
deleted file mode 100644
index 838133233b3d3c81c03482bab0ff5776c9689b9b..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 498154
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