Skip to content

Instantly share code, notes, and snippets.

@saroar
Created February 3, 2024 13:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save saroar/daa1ee445871b9e309cfb170d05b5f96 to your computer and use it in GitHub Desktop.
Save saroar/daa1ee445871b9e309cfb170d05b5f96 to your computer and use it in GitHub Desktop.
//
// LearnPlayGrowApp.swift
// LearnPlayGrow
//
// Created by Saroar Khandoker on 20.03.2023.
//
import UIKit
import SwiftUI
import AppView
import AppFeature
import ComposableArchitecture
final class AppDelegate: NSObject, UIApplicationDelegate {
let store = Store(initialState: AppReducer.State()) {
AppReducer()._printChanges()
}
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
self.store.send(.appDelegate(.didFinishLaunching))
return true
}
func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
self.store.send(.appDelegate(.didRegisterForRemoteNotifications(.success(deviceToken))))
}
func application(
_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error
) {
self.store.send(.appDelegate(.didRegisterForRemoteNotifications(.failure(error))))
}
}
@main
struct LearnPlayGrowApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
@Environment(\.scenePhase) private var scenePhase
var body: some Scene {
WindowGroup {
AppView(store: self.appDelegate.store)
}
.onChange(of: self.scenePhase) {
self.appDelegate.store.send(.didChangeScenePhase($0))
}
}
}
@Reducer
public struct AppDelegateReducer {
public struct State: Equatable {
public init() {}
}
public enum Action: Equatable {
case didFinishLaunching
case didRegisterForRemoteNotifications(TaskResult<Data>)
case userNotifications(UserNotificationClient.DelegateEvent)
case userSettingsLoaded(TaskResult<UserSettings>)
case deviceResponse(TaskResult<DeviceInOutPut>)
case getNotificationSettings
case createOrUpdate(deviceToken: Data)
}
@Dependency(\.apiClient) var apiClient
@Dependency(\.build.number) var buildNumber
@Dependency(\.remoteNotifications) var remoteNotifications
@Dependency(\.applicationClient.setUserInterfaceStyle) var setUserInterfaceStyle
@Dependency(\.userNotifications) var userNotifications
public init() {}
public func reduce(into state: inout State, action: Action) -> Effect<Action> {
switch action {
case .didFinishLaunching:
return .run { send in
await withThrowingTaskGroup(of: Void.self) { group in
group.addTask {
for await event in self.userNotifications.delegate() {
await send(.userNotifications(event))
}
}
group.addTask {
let settings = await self.userNotifications.getNotificationSettings()
switch settings.authorizationStatus {
case .authorized:
guard
try await self.userNotifications.requestAuthorization([.alert, .badge, .sound])
else { return }
case .notDetermined, .provisional:
guard try await self.userNotifications.requestAuthorization(.provisional)
else { return }
default:
return
}
}
group.addTask {
await registerForRemoteNotificationsAsync(
remoteNotifications: self.remoteNotifications,
userNotifications: self.userNotifications
)
}
}
}
case let .didRegisterForRemoteNotifications(.success(data)):
return .run { send in
await send(.getNotificationSettings)
await send(.createOrUpdate(deviceToken: data))
}
case .didRegisterForRemoteNotifications(.failure):
return .none
case .getNotificationSettings:
logger.info("\(#line) run getNotificationSettings")
return .run { _ in
_ = await self.userNotifications.getNotificationSettings()
}
case let .createOrUpdate(deviceToken: data):
let identifierForVendor = UIDevice.current.identifierForVendor?.uuidString
let token = data.toHexString
let device = DeviceInOutPut(
identifierForVendor: identifierForVendor,
name: UIDevice.current.name,
model: UIDevice.current.model,
osVersion: UIDevice.current.systemVersion,
pushToken: token,
voipToken: ""
)
return .run { send in
await send(.deviceResponse(
await TaskResult {
try await apiClient.request(
for: .authEngine(.devices(.createOrUpdate(input: device))),
as: DeviceInOutPut.self,
decoder: .iso8601
)
}
))
}
case let .userNotifications(.willPresentNotification(_, completionHandler)):
return .run { _ in completionHandler(.banner) }
case .userNotifications:
return .none
case .userSettingsLoaded:
return .none
// var newState = result
// return .run { _ in
// async let setUI: Void =
// await self.setUserInterfaceStyle(newState.colorScheme.userInterfaceStyle)
// _ = await setUI
//
// }
case .deviceResponse(.success):
logger.info("\(#line) deviceResponse success")
return .none
case .deviceResponse(.failure(let error)):
logger.error("\(#line) deviceResponse error \(error.localizedDescription)")
return .none
}
}
}
public let logger = Logger(subsystem: "com.addame.AddaMeIOS", category: "appDelegate.reducer")
@Reducer
public struct AppReducer {
@ObservableState
public struct State: Equatable {
public init(
startup: Startup.State = .login(.init()),
appDelegate: AppDelegateReducer.State = .init()
) {
self.startup = startup
self.appDelegate = appDelegate
}
public var startup: Startup.State
public var appDelegate: AppDelegateReducer.State
}
@CasePathable
public enum Action {
case startup(Startup.Action)
case appDelegate(AppDelegateReducer.Action)
case didChangeScenePhase(ScenePhase)
}
@Dependency(\.userDefaults) var userDefaults
@Dependency(\.userNotifications) var userNotifications
@Dependency(\.remoteNotifications) var remoteNotifications
@Dependency(\.mainRunLoop) var mainRunLoop
@Dependency(\.keychainClient) var keychainClient
@Dependency(\.build) var build
public init() {}
public var body: some Reducer<State, Action> {
Scope(state: \.appDelegate, action: /Action.appDelegate) {
AppDelegateReducer()
}
Scope(state: \.startup, action: \.startup) {
Startup()
}
Reduce(self.core)
}
func core(state: inout State, action: Action) -> Effect<Action> {
switch action {
case .startup:
return .none
case .appDelegate:
return .none
case .didChangeScenePhase:
return .none
}
}
}
import SwiftUI
import TabFeature
import ProfileView
import KeychainClient
import LocationReducer
import LPGSharedModels
import ConversationsView
import AuthenticationView
import UserDefaultsClient
import AuthenticationCore
import NotificationHelpers
import RegisterFormFeature
import ComposableArchitecture
@Reducer
public struct Startup {
@ObservableState
public enum State: Equatable {
case login(Login.State)
case register(RegisterFormReducer.State)
case tabbar(TabReducer.State)
}
public enum Action: Equatable {
case onAppear
case login(Login.Action)
case register(RegisterFormReducer.Action)
case tabbar(TabReducer.Action)
case move(Move)
public enum Move {
case login, register, tabbar
}
}
public init() {}
@Dependency(\.userDefaults) var userDefaults
public var body: some Reducer<State, Action> {
Scope(state: \.login, action: \.login) {
Login()
}
Scope(state: \.register, action: \.register) {
RegisterFormReducer()
}
Scope(state: \.tabbar, action: \.tabbar) {
TabReducer()
}
Reduce(self.core)
}
func core(state: inout State, action: Action) -> Effect<Action> {
switch action {
case .onAppear:
let isAuthorized = userDefaults.boolForKey(UserDefaultKey.isAuthorized.rawValue) == true
let isAskPermissionCompleted = userDefaults.boolForKey(UserDefaultKey.isAskPermissionCompleted.rawValue) == true
if isAuthorized && isAskPermissionCompleted {
return .run { send in
await send(.move(.tabbar))
}
} else if isAuthorized && !isAskPermissionCompleted {
return .run { send in
await send(.move(.register))
}
} else {
return .run { send in
await send(.move(.login))
}
}
case .login(.moveToPermissionsFrom):
state = .register(.init())
return .none
case .login:
state = .login(.init())
return .none
case .register(.moveToTabberView):
state = .tabbar(.init())
return .none
case .register:
state = .register(.init())
return .none
case .tabbar:
state = .tabbar(.init())
return .none
case .move:
return .none
}
}
}
import SwiftUI
public struct StartupView: View {
public let store: StoreOf<Startup>
public init(store: StoreOf<Startup>) {
self.store = store
}
public var body: some View {
WithPerceptionTracking {
ZStack(alignment: .center) {
switch self.store.state {
case .login:
if let store = self.store.scope(state: \.login, action: \.login) {
AuthenticationView(store: store)
}
case .register:
if let store = self.store.scope(state: \.register, action: \.register) {
RegisterFormView(store: store)
}
case .tabbar:
if let store = self.store.scope(state: \.tabbar, action: \.tabbar) {
NavigationView {
TabBarView(store: store)
}
}
}
}
}
.onAppear {
store.send(.onAppear)
}
}
}
#if DEBUG
//struct StartupView_Previews: PreviewProvider {
// static var previews: some View {
// StartupView(store: StoreOf<Startup>(
// initialState: Startup.State()
// ){
// Startup()
// })
// }
//}
#endif
import AVFoundation
import Contacts
import CoreLocation
import Foundation
import UserNotifications
@DependencyClient
public struct DevicePermissions {
enum PermissionError: Error {
case cameraAccessDenied
case microphoneAccessDenied
case contactAccessDenied
case locationAccessDenied
case notificationAccessDenied
}
public var requestCameraPermissions: @Sendable () async throws -> Bool
public var requestMicrophonePermissions: @Sendable () async throws -> Bool
public var requestContactPermissions: @Sendable () async throws -> Bool
public var requestLocationPermissions: @Sendable () async throws -> Bool
public var requestNotificationPermissions: @Sendable () async throws -> Bool
}
extension DevicePermissions {
public static var live: DevicePermissions = .init(
requestCameraPermissions: {
let status = AVCaptureDevice.authorizationStatus(for: .video)
// Determine if the user previously authorized camera access.
var isAuthorized = status == .authorized
// If the system hasn't determined the user's authorization status,
// explicitly prompt them for approval.
if status == .notDetermined {
isAuthorized = await AVCaptureDevice.requestAccess(for: .video)
}
return isAuthorized
},
requestMicrophonePermissions: {
try await withCheckedThrowingContinuation { continuation in
AVCaptureDevice.requestAccess(for: .audio) { granted in
if granted {
continuation.resume(returning: true)
} else {
continuation.resume(throwing: PermissionError.microphoneAccessDenied)
}
}
}
},
requestContactPermissions: {
try await withCheckedThrowingContinuation { continuation in
let store = CNContactStore()
store.requestAccess(for: .contacts) { granted, error in
if let error = error {
continuation.resume(throwing: error)
} else if granted {
continuation.resume(returning: true)
} else {
continuation.resume(throwing: PermissionError.contactAccessDenied)
}
}
}
},
requestLocationPermissions: {
return true
},
requestNotificationPermissions: {
try await withCheckedThrowingContinuation { continuation in
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
if let error = error {
continuation.resume(throwing: error)
} else if granted {
continuation.resume(returning: true)
} else {
continuation.resume(throwing: PermissionError.notificationAccessDenied)
}
}
}
}
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment