diff --git a/.github/workflows/ios-tests.yml b/.github/workflows/ios-tests.yml index a48d911e..11bac092 100644 --- a/.github/workflows/ios-tests.yml +++ b/.github/workflows/ios-tests.yml @@ -75,7 +75,10 @@ jobs: - name: Checkout uses: actions/checkout@v2 - name: Build and Test - run: swift package generate-xcodeproj && xcrun xcodebuild test -scheme "Vexil-Package" -destination "platform=iOS Simulator,name=iPhone 8" + run: | + DEVICE_ID=`xcrun simctl list --json devices available iPhone | jq -r '.devices | to_entries | map(select(.value | add)) | sort_by(.key) | last.value | first.udid'` + swift package generate-xcodeproj + xcrun xcodebuild test -scheme "Vexil-Package" -destination "platform=iOS Simulator,id=$DEVICE_ID" build-ios-macos-12: runs-on: ubuntu-latest diff --git a/Sources/Vexil/Sources/UserDefaults+FlagValueSource.swift b/Sources/Vexil/Sources/UserDefaults+FlagValueSource.swift index 2726e22c..d441806f 100644 --- a/Sources/Vexil/Sources/UserDefaults+FlagValueSource.swift +++ b/Sources/Vexil/Sources/UserDefaults+FlagValueSource.swift @@ -46,7 +46,11 @@ extension UserDefaults: FlagValueSource { return } - set(value.boxedFlagValue.object, forKey: key) + if value.boxedFlagValue.object == NSNull() { + set(Data(), forKey: key) + } else { + set(value.boxedFlagValue.object, forKey: key) + } } diff --git a/Sources/Vexillographer/Flag Value Controls/CaseIterableFlagControl.swift b/Sources/Vexillographer/Flag Value Controls/CaseIterableFlagControl.swift index 8d6b4f2c..ac5e00b3 100644 --- a/Sources/Vexillographer/Flag Value Controls/CaseIterableFlagControl.swift +++ b/Sources/Vexillographer/Flag Value Controls/CaseIterableFlagControl.swift @@ -34,9 +34,6 @@ struct CaseIterableFlagControl: View where Value: FlagValue, Value: CaseI @Binding var showDetail: Bool - @Binding - var showPicker: Bool - // MARK: - View Body var content: some View { @@ -52,7 +49,7 @@ struct CaseIterableFlagControl: View where Value: FlagValue, Value: CaseI var body: some View { HStack { if self.isEditable { - NavigationLink(destination: self.selector, isActive: self.$showPicker) { + NavigationLink(destination: self.selector) { self.content } } else { @@ -63,7 +60,7 @@ struct CaseIterableFlagControl: View where Value: FlagValue, Value: CaseI } var selector: some View { - return self.selectorList + SelectorList(value: self.$value) .navigationBarTitle(Text(self.label), displayMode: .inline) } @@ -104,52 +101,58 @@ struct CaseIterableFlagControl: View where Value: FlagValue, Value: CaseI #endif - var selectorList: some View { - Form { - ForEach(Value.allCases, id: \.self) { value in - Button( - action: { - self.value = value - self.showPicker = false - }, - label: { - HStack { - FlagDisplayValueView(value: value) - .foregroundColor(.primary) - Spacer() - - if value == self.value { - self.checkmark + struct SelectorList: View { + @Binding + var value: Value + + @Environment(\.presentationMode) + private var presentationMode + + var body: some View { + Form { + ForEach(Value.allCases, id: \.self) { value in + Button( + action: { + self.value = value + self.presentationMode.wrappedValue.dismiss() + }, + label: { + HStack { + FlagDisplayValueView(value: value) + .foregroundColor(.primary) + Spacer() + + if value == self.value { + self.checkmark + } } } - } - ) + ) + } } } - } #if os(macOS) - var checkmark: some View { - return Text("✓") - } + var checkmark: some View { + return Text("✓") + } #else - var checkmark: some View { - return Image(systemName: "checkmark") - } + var checkmark: some View { + return Image(systemName: "checkmark") + } #endif - + } } - // MARK: - Creating CaseIterableFlagControls @available(OSX 11.0, iOS 13.0, watchOS 7.0, tvOS 13.0, *) protocol CaseIterableEditableFlag { - func control(label: String, manager: FlagValueManager, showDetail: Binding, showPicker: Binding) -> AnyView where RootGroup: FlagContainer + func control(label: String, manager: FlagValueManager, showDetail: Binding) -> AnyView where RootGroup: FlagContainer } @available(OSX 11.0, iOS 13.0, watchOS 7.0, tvOS 13.0, *) @@ -157,7 +160,7 @@ extension UnfurledFlag: CaseIterableEditableFlag where Value: FlagValue, Value: CaseIterable, Value.AllCases: RandomAccessCollection, Value: RawRepresentable, Value.RawValue: FlagValue, Value: Hashable { - func control(label: String, manager: FlagValueManager, showDetail: Binding, showPicker: Binding) -> AnyView where RootGroup: FlagContainer { + func control(label: String, manager: FlagValueManager, showDetail: Binding) -> AnyView where RootGroup: FlagContainer { return CaseIterableFlagControl( label: label, value: Binding( @@ -168,8 +171,7 @@ extension UnfurledFlag: CaseIterableEditableFlag ), hasChanges: manager.hasValueInSource(flag: flag), isEditable: manager.isEditable, - showDetail: showDetail, - showPicker: showPicker + showDetail: showDetail ) .eraseToAnyView() } diff --git a/Sources/Vexillographer/Flag Value Controls/OptionalCaseIterableFlagControl.swift b/Sources/Vexillographer/Flag Value Controls/OptionalCaseIterableFlagControl.swift index 7deae0d0..1a11c743 100644 --- a/Sources/Vexillographer/Flag Value Controls/OptionalCaseIterableFlagControl.swift +++ b/Sources/Vexillographer/Flag Value Controls/OptionalCaseIterableFlagControl.swift @@ -37,9 +37,6 @@ struct OptionalCaseIterableFlagControl: View @Binding var showDetail: Bool - @Binding - var showPicker: Bool - // MARK: - View Body var content: some View { @@ -53,7 +50,7 @@ struct OptionalCaseIterableFlagControl: View var body: some View { HStack { if self.isEditable { - NavigationLink(destination: self.selector, isActive: self.$showPicker) { + NavigationLink(destination: self.selector) { self.content } } else { @@ -66,82 +63,92 @@ struct OptionalCaseIterableFlagControl: View #if os(iOS) var selector: some View { - return self.selectorList + SelectorList(value: self.$value) .navigationBarTitle(Text(self.label), displayMode: .inline) } #else var selector: some View { - return self.selectorList + SelectorList(value: self.$value) } #endif - var selectorList: some View { - Form { - Section { - Button( - action: { - self.valueSelected(nil) - }, - label: { - Text("None") - .foregroundColor(.primary) - Spacer() - - if self.value.wrapped == nil { - self.checkmark + struct SelectorList: View { + @Binding + var value: Value + + @Environment(\.presentationMode) + private var presentationMode + + var body: some View { + Form { + Section { + Button( + action: { + self.valueSelected(nil) + }, + label: { + HStack { + Text("None") + .foregroundColor(.primary) + Spacer() + + if self.value.wrapped == nil { + self.checkmark + } + } } - } - ) - } + ) + } - ForEach(Value.WrappedFlagValue.allCases, id: \.self) { value in - Button( - action: { - self.valueSelected(value) - }, - label: { - FlagDisplayValueView(value: value) - Spacer() - - if value == self.value.wrapped { - self.checkmark + ForEach(Value.WrappedFlagValue.allCases, id: \.self) { value in + Button( + action: { + self.valueSelected(value) + }, + label: { + HStack { + FlagDisplayValueView(value: value) + .foregroundColor(.primary) + Spacer() + + if value == self.value.wrapped { + self.checkmark + } + } } - } - ) + ) + } } } - } #if os(macOS) - var checkmark: some View { - return Text("✓") - } + var checkmark: some View { + return Text("✓") + } #else - var checkmark: some View { - return Image(systemName: "checkmark") - } + var checkmark: some View { + return Image(systemName: "checkmark") + } #endif - - func valueSelected(_ value: Value.WrappedFlagValue?) { - self.value.wrapped = value - showPicker = false + func valueSelected(_ value: Value.WrappedFlagValue?) { + self.value.wrapped = value + presentationMode.wrappedValue.dismiss() + } } - } - // MARK: - Creating CaseIterableFlagControls @available(OSX 11.0, iOS 13.0, watchOS 7.0, tvOS 13.0, *) protocol OptionalCaseIterableEditableFlag { - func control(label: String, manager: FlagValueManager, showDetail: Binding, showPicker: Binding) -> AnyView where RootGroup: FlagContainer + func control(label: String, manager: FlagValueManager, showDetail: Binding) -> AnyView where RootGroup: FlagContainer } @available(OSX 11.0, iOS 13.0, watchOS 7.0, tvOS 13.0, *) @@ -150,7 +157,7 @@ extension UnfurledFlag: OptionalCaseIterableEditableFlag Value.WrappedFlagValue.AllCases: RandomAccessCollection, Value.WrappedFlagValue: RawRepresentable, Value.WrappedFlagValue.RawValue: FlagValue, Value.WrappedFlagValue: Hashable { - func control(label: String, manager: FlagValueManager, showDetail: Binding, showPicker: Binding) -> AnyView where RootGroup: FlagContainer { + func control(label: String, manager: FlagValueManager, showDetail: Binding) -> AnyView where RootGroup: FlagContainer { let key = info.key return OptionalCaseIterableFlagControl( @@ -159,7 +166,7 @@ extension UnfurledFlag: OptionalCaseIterableEditableFlag get: { Value(manager.flagValue(key: key)) }, set: { newValue in do { - try manager.setFlagValue(newValue.wrapped, key: key) + try manager.setFlagValue(newValue, key: key) } catch { print("[Vexilographer] Could not set flag with key \"\(key)\" to \"\(newValue)\"") @@ -168,8 +175,7 @@ extension UnfurledFlag: OptionalCaseIterableEditableFlag ), hasChanges: manager.hasValueInSource(flag: flag), isEditable: manager.isEditable, - showDetail: showDetail, - showPicker: showPicker + showDetail: showDetail ) .eraseToAnyView() } diff --git a/Sources/Vexillographer/FlagGroupView.swift b/Sources/Vexillographer/FlagGroupView.swift index 1f9fc21e..b804e468 100644 --- a/Sources/Vexillographer/FlagGroupView.swift +++ b/Sources/Vexillographer/FlagGroupView.swift @@ -63,7 +63,7 @@ struct UnfurledFlagGroupView: View where Group: FlagContainer, Root Section { // Filter out all links. They won't work on the mac flag group view. ForEach(self.group.allItems().filter { $0.isLink == false }, id: \.id) { item in - item.unfurledView + UnfurledFlagItemView(item: item) } } } @@ -98,7 +98,7 @@ struct UnfurledFlagGroupView: View where Group: FlagContainer, Root var flags: some View { ForEach(self.group.allItems(), id: \.id) { item in - item.unfurledView + UnfurledFlagItemView(item: item) } } diff --git a/Sources/Vexillographer/FlagSectionView.swift b/Sources/Vexillographer/FlagSectionView.swift index 7262181a..7b8b815f 100644 --- a/Sources/Vexillographer/FlagSectionView.swift +++ b/Sources/Vexillographer/FlagSectionView.swift @@ -68,7 +68,7 @@ struct UnfurledFlagSectionView: View where Group: FlagContainer, Ro private var content: some View { ForEach(self.group.allItems(), id: \.id) { item in - item.unfurledView + UnfurledFlagItemView(item: item) } } diff --git a/Sources/Vexillographer/FlagView.swift b/Sources/Vexillographer/FlagView.swift index 433fd92d..254e6852 100644 --- a/Sources/Vexillographer/FlagView.swift +++ b/Sources/Vexillographer/FlagView.swift @@ -29,10 +29,6 @@ struct UnfurledFlagView: View where Value: FlagValue, RootGrou @State private var showDetail = false - @State - private var showPicker = false - - // MARK: - Initialisation init(flag: UnfurledFlag, manager: FlagValueManager) { @@ -65,10 +61,10 @@ struct UnfurledFlagView: View where Value: FlagValue, RootGrou return flag.control(label: self.flag.info.name, manager: self.manager, showDetail: self.$showDetail) } else if let flag = self.flag as? CaseIterableEditableFlag { - return flag.control(label: self.flag.info.name, manager: self.manager, showDetail: self.$showDetail, showPicker: self.$showPicker) + return flag.control(label: self.flag.info.name, manager: self.manager, showDetail: self.$showDetail) } else if let flag = self.flag as? OptionalCaseIterableEditableFlag { - return flag.control(label: self.flag.info.name, manager: self.manager, showDetail: self.$showDetail, showPicker: self.$showPicker) + return flag.control(label: self.flag.info.name, manager: self.manager, showDetail: self.$showDetail) } else if let flag = self.flag as? StringEditableFlag { return flag.control(label: self.flag.info.name, manager: self.manager, showDetail: self.$showDetail) diff --git a/Sources/Vexillographer/Unfurling/UnfurledFlagItem.swift b/Sources/Vexillographer/Unfurling/UnfurledFlagItem.swift index 03278eab..06552d6a 100644 --- a/Sources/Vexillographer/Unfurling/UnfurledFlagItem.swift +++ b/Sources/Vexillographer/Unfurling/UnfurledFlagItem.swift @@ -28,4 +28,13 @@ protocol UnfurledFlagItem { var isLink: Bool { get } } +@available(OSX 11.0, iOS 13.0, watchOS 7.0, tvOS 13.0, *) +struct UnfurledFlagItemView: View { + var item: UnfurledFlagItem + + var body: some View { + item.unfurledView.id(item.id) + } +} + #endif diff --git a/Sources/Vexillographer/Vexillographer.swift b/Sources/Vexillographer/Vexillographer.swift index ad04d60b..04e5c69c 100644 --- a/Sources/Vexillographer/Vexillographer.swift +++ b/Sources/Vexillographer/Vexillographer.swift @@ -16,6 +16,8 @@ import SwiftUI import Vexil +#if os(macOS) && compiler(>=5.3.1) + /// A SwiftUI View that allows you to easily edit the flag /// structure in a provided FlagValueSource. @available(OSX 11.0, iOS 13.0, watchOS 7.0, tvOS 13.0, *) @@ -26,7 +28,6 @@ public struct Vexillographer: View where RootGroup: FlagContainer { @ObservedObject var manager: FlagValueManager - // MARK: - Initialisation /// Initialises a new `Vexillographer` instance with the provided FlagPole and source @@ -36,17 +37,14 @@ public struct Vexillographer: View where RootGroup: FlagContainer { /// - source: An optional `FlagValueSource` for editing the flag values in. If `nil` the flag values are displayed read-only /// public init(flagPole: FlagPole, source: FlagValueSource?) { - self.manager = FlagValueManager(flagPole: flagPole, source: source) + self._manager = ObservedObject(wrappedValue: FlagValueManager(flagPole: flagPole, source: source)) } - // MARK: - Body -#if os(macOS) && compiler(>=5.3.1) - public var body: some View { List(self.manager.allItems(), id: \.id, children: \.childLinks) { item in - item.unfurledView + UnfurledFlagItemView(item: item) } .listStyle(SidebarListStyle()) .toolbar { @@ -57,17 +55,40 @@ public struct Vexillographer: View where RootGroup: FlagContainer { } } } +} #else +/// A SwiftUI View that allows you to easily edit the flag +/// structure in a provided FlagValueSource. +@available(OSX 11.0, iOS 13.0, watchOS 7.0, tvOS 13.0, *) +public struct Vexillographer: View where RootGroup: FlagContainer { + + // MARK: - Properties + + @State + var manager: FlagValueManager + + // MARK: - Initialisation + + /// Initialises a new `Vexillographer` instance with the provided FlagPole and source + /// + /// - Parameters; + /// - flagPole: A `FlagPole` instance manages the flag and source hierarchy we want to display + /// - source: An optional `FlagValueSource` for editing the flag values in. If `nil` the flag values are displayed read-only + /// + public init(flagPole: FlagPole, source: FlagValueSource?) { + self._manager = State(wrappedValue: FlagValueManager(flagPole: flagPole, source: source)) + } + public var body: some View { ForEach(self.manager.allItems(), id: \.id) { item in - item.unfurledView + UnfurledFlagItemView(item: item) } .environmentObject(self.manager) } +} #endif -} #endif