QuickTime Movie (4.9MB)
Last active
August 3, 2020 05:19
-
-
Save end3r117/454759ad8628055ee371d8dc5f44283c to your computer and use it in GitHub Desktop.
20200715 - SwiftUI | Combine | Reddit demo
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // 20200715_ShutterSpeedDemo.swift | |
| // MuckAbout | |
| // | |
| // Created by Anthony Rosario on 7/15/20. | |
| // | |
| import Combine | |
| import SwiftUI | |
| extension UserDefaults { | |
| @objc dynamic var shutterSpeedIncrement: ShutterSpeedIncrement { | |
| let int = integer(forKey: ShutterSpeedIncrement.userDefaultsKey) | |
| return ShutterSpeedIncrement(rawValue: int) ?? .third //default | |
| } | |
| } | |
| protocol PickerOption: Identifiable, Hashable { | |
| associatedtype Value: Hashable | |
| var id: ID { get } | |
| var value: Value { get } | |
| var text: String { get } | |
| } | |
| @objc | |
| enum ShutterSpeedIncrement: Int, CaseIterable { | |
| static var userDefaultsKey: String = "shutterSpeedIncrement" | |
| case third = 0, half = 1, full = 2 | |
| struct ShutterSpeed: PickerOption { | |
| let id: String | |
| let value: Int | |
| let text: String | |
| init(increment: ShutterSpeedIncrement, speed: Int) { | |
| self.id = "\(increment.stringValue) - \(speed)" | |
| self.value = speed | |
| self.text = "\(speed)" | |
| } | |
| } | |
| var stringValue: String { | |
| switch self { | |
| case .third: return "1/3" | |
| case .half: return "1/2" | |
| case .full: return "Full" | |
| } | |
| } | |
| var shutterSpeeds: [ShutterSpeed] { | |
| __shutterSpeeds.map({ShutterSpeed(increment: self, speed: $0)}) | |
| } | |
| private var __shutterSpeeds: [Int] { | |
| //inverse | |
| switch self { | |
| case .third: return [5000, 4000, 3200, 2500, 2000, 1600, 1000, 800, 640, 500, 400, 320, 250, 200, 160, 125, 100] | |
| case .half: return [6000, 4000, 3000, 2000, 1500, 1000, 750, 500, 350, 250, 180, 125, 90] | |
| case .full: return [8000, 4000, 2000, 1000, 500, 250, 60, 30, 15, 8, 4] | |
| } | |
| } | |
| } | |
| typealias ShutterSpeed = ShutterSpeedIncrement.ShutterSpeed | |
| class HomeViewModel: ObservableObject { | |
| @Published var shutterSpeeds: [ShutterSpeed] | |
| @Published var selectedSpeed: ShutterSpeed? | |
| private var currentShutterSpeedIncrement: ShutterSpeedIncrement | |
| private var shutterSub: AnyCancellable? | |
| init() { | |
| let stored = UserDefaults.standard.shutterSpeedIncrement | |
| self.currentShutterSpeedIncrement = stored | |
| self.shutterSpeeds = stored.shutterSpeeds | |
| self.selectedSpeed = stored.shutterSpeeds.first | |
| shutterSub = UserDefaults.standard.publisher(for: \.shutterSpeedIncrement) | |
| .receive(on: DispatchQueue.main) | |
| .print("HomeVM") | |
| .map({$0.shutterSpeeds}) | |
| .sink(receiveValue: {[weak self] value in | |
| DispatchQueue.main.async { | |
| withAnimation { | |
| self?.shutterSpeeds = value | |
| self?.selectedSpeed = value.first | |
| } | |
| } | |
| }) | |
| } | |
| } | |
| struct PickerOptionsStack<OptionType: PickerOption>: View { | |
| @Environment(\.colorScheme) var colorScheme | |
| @Binding var selectedOption: OptionType? | |
| @Binding var options: [OptionType] | |
| let heading: String | |
| var backgroundColor: Color = Color(.secondarySystemBackground) | |
| private var _buttonColor: Color? | |
| var buttonColor: Color { | |
| _buttonColor ?? (colorScheme == .dark ? Color(.systemFill) : Color(.systemGray4)) | |
| } | |
| init(heading: String, selectedOption: Binding<OptionType?>, options: Binding<[OptionType]>, backgroundColor: Color? = nil, buttonColor: Color? = nil) { | |
| self.heading = heading | |
| self._selectedOption = selectedOption | |
| self._options = options | |
| self._buttonColor = buttonColor | |
| if let color = backgroundColor { | |
| self.backgroundColor = color | |
| } | |
| } | |
| var body: some View { | |
| GeometryReader { geo in | |
| VStack { | |
| Text(heading) | |
| .font(.headline) | |
| .multilineTextAlignment(.center) | |
| .padding([.horizontal, .top]) | |
| Divider() | |
| ScrollView(showsIndicators: false) { | |
| ScrollViewReader { reader in | |
| VStack(spacing: 12) { | |
| Spacer() | |
| .frame(maxHeight: 4) | |
| ForEach(options, id: \.id){ option in | |
| Button(action: { | |
| selectedOption = option | |
| scrollTo(option, readerProxy: reader) | |
| }, label: { | |
| Text(option.text) | |
| .font(.subheadline) | |
| .fontWeight(selectedOption == option ? .bold : .light) | |
| //frame is leftover but worked okay so I left it | |
| .frame(width: max(geo.size.height / CGFloat(options.count), 60), height: 30) | |
| .padding(.horizontal, 8) | |
| .foregroundColor(selectedOption == option ? .accentColor : .secondary) | |
| }) | |
| .tag(option.id) | |
| Divider() | |
| } | |
| .padding(.horizontal) | |
| } | |
| .onChange(of: options) {newValue in | |
| guard !newValue.isEmpty else { return } | |
| scrollTo(options.last, readerProxy: reader) | |
| DispatchQueue.main.asyncAfter(deadline: .now() + 1) { | |
| scrollTo(options.first, readerProxy: reader) | |
| } | |
| } | |
| } | |
| .padding(.bottom) | |
| } | |
| } | |
| .background( | |
| GeometryReader { geo in | |
| RoundedRectangle(cornerRadius: 4, style: .continuous) | |
| .fill(Color(colorScheme == .dark ? .secondarySystemBackground : .systemGroupedBackground)) | |
| } | |
| ) | |
| .frame(maxWidth: geo.size.width * 0.33) | |
| } | |
| } | |
| private func scrollTo(_ option: OptionType?, readerProxy proxy: ScrollViewProxy) { | |
| guard let option = option else { return } | |
| DispatchQueue.main.async { | |
| withAnimation(.easeOut(duration: 1)) { | |
| proxy.scrollTo(option.id, anchor: UnitPoint(x: 0.5, y: 0.25)) | |
| } | |
| } | |
| } | |
| } | |
| class SettingsDemoVM: ObservableObject { | |
| @Published var shutterSpeedIncrement: ShutterSpeedIncrement | |
| @Published var otherSetting1: Int = 0 | |
| @Published var otherSetting2: Bool = false | |
| private var settingsSubscription: AnyCancellable? | |
| init() { | |
| shutterSpeedIncrement = UserDefaults.standard.shutterSpeedIncrement | |
| settingsSubscription = Publishers | |
| .CombineLatest3($shutterSpeedIncrement, $otherSetting1, $otherSetting2) | |
| .receive(on: DispatchQueue.main) | |
| .sink { (output) in | |
| let speed = output.0, settings1 = output.1, settings2 = output.2 | |
| DispatchQueue.main.async { | |
| UserDefaults.standard.set(speed.rawValue, | |
| forKey: ShutterSpeedIncrement.userDefaultsKey) | |
| UserDefaults.standard.set(settings1, forKey: "OtherSettings1") | |
| UserDefaults.standard.set(settings2, forKey: "OtherSettings2") | |
| } | |
| } | |
| } | |
| } | |
| struct SettingsDemo: View { | |
| @StateObject var viewModel = SettingsDemoVM() | |
| var body: some View { | |
| GeometryReader { geo in | |
| VStack(spacing: 0) { | |
| Section(header: header){ | |
| Form { | |
| Picker( | |
| selection: $viewModel.shutterSpeedIncrement, | |
| label: | |
| Text("Shutter Speed Increment") | |
| .foregroundColor(.primary) | |
| ){ | |
| ForEach(ShutterSpeedIncrement.allCases, id: \.self) { increment in | |
| Text(increment.stringValue) | |
| .tag(increment) | |
| .foregroundColor(.accentColor) | |
| .frame(height: 40) | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| var header: some View { | |
| VStack { | |
| HStack { | |
| Text("Settings View") | |
| .font(.title2) | |
| .padding(.leading, 4) | |
| Spacer() | |
| } | |
| .padding(.leading) | |
| Divider() | |
| } | |
| } | |
| } | |
| struct HomeView: View { | |
| @Environment(\.colorScheme) var colorScheme | |
| @StateObject var viewModel = HomeViewModel() | |
| var body: some View { | |
| GeometryReader { geo in | |
| VStack { | |
| Section(header: header) { | |
| PickerOptionsStack<ShutterSpeed>( | |
| heading: "Shutter Speed", | |
| selectedOption: $viewModel.selectedSpeed, | |
| options: $viewModel.shutterSpeeds, | |
| buttonColor: nil) | |
| .padding() | |
| } | |
| } | |
| } | |
| } | |
| var header: some View { | |
| VStack { | |
| HStack { | |
| Text("Home View") | |
| .font(.title2) | |
| .padding(.leading, 4) | |
| Spacer() | |
| } | |
| Divider() | |
| }.padding(.leading) | |
| } | |
| } | |
| enum ActiveView: Int, Hashable { | |
| case combo, home, settings | |
| } | |
| struct ShutterSpeedDemo: View { | |
| @Environment(\.colorScheme) var colorScheme | |
| @State private var comboMode: Bool = false | |
| @State private var activeView: ActiveView = .home | |
| var body: some View { | |
| TabView(selection: $activeView) { | |
| if comboMode { | |
| comboTab | |
| }else { | |
| homeTab | |
| settingsTab | |
| } | |
| } | |
| .animation(.easeOut) | |
| .colorScheme(colorScheme) | |
| } | |
| var navButton: some View { | |
| Button(comboMode ? "Combo" : "Split"){ | |
| DispatchQueue.main.async { | |
| comboMode.toggle() | |
| } | |
| } | |
| .foregroundColor(.accentColor) | |
| .padding(.trailing) | |
| } | |
| var comboTab: some View { | |
| VStack(spacing: 0) { | |
| NavigationView { | |
| homeView | |
| .navigationBarTitle(Text("Shutter Speed Demo")) | |
| .navigationBarItems(trailing: navButton) | |
| } | |
| NavigationView { | |
| settingsView | |
| .navigationBarTitle("Settings View") | |
| .navigationBarHidden(true) | |
| } | |
| } | |
| .tabItem { Label("Combo", systemImage: "star").tag(ActiveView.combo) } | |
| } | |
| let homeView: HomeView = HomeView() | |
| var homeTab: some View { | |
| NavigationView { | |
| homeView | |
| .navigationBarTitle(Text("Shutter Speed Demo")) | |
| .navigationBarItems(trailing: navButton) | |
| } | |
| .foregroundColor(.primary) | |
| .tabItem { Label("Home", systemImage: "house.fill").tag(ActiveView.home) } | |
| } | |
| let settingsView: SettingsDemo = SettingsDemo() | |
| var settingsTab: some View { | |
| NavigationView { | |
| settingsView | |
| .navigationBarTitle(Text("Shutter Speed Demo")) | |
| .navigationBarItems(trailing: navButton) | |
| } | |
| .tabItem { Label("Settings", systemImage: "gear").tag(ActiveView.settings) } | |
| } | |
| } | |
| struct HomeView_Previews: PreviewProvider { | |
| static var previews: some View { | |
| ShutterSpeedDemo() | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment