Skip to content

Instantly share code, notes, and snippets.

@end3r117
Last active August 3, 2020 05:19
Show Gist options
  • Save end3r117/454759ad8628055ee371d8dc5f44283c to your computer and use it in GitHub Desktop.
Save end3r117/454759ad8628055ee371d8dc5f44283c to your computer and use it in GitHub Desktop.
20200715 - SwiftUI | Combine | Reddit demo
//
// 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