SwiftUI Router Setup

This commit is contained in:
Francisco Gindre 2021-08-06 20:38:47 -03:00
parent 52d3b8610a
commit 15ef90cb40
17 changed files with 640 additions and 1 deletions

View File

@ -7,6 +7,14 @@
objects = {
/* Begin PBXBuildFile section */
0D170A7226BC802800EB6A46 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D170A7126BC802800EB6A46 /* Router.swift */; };
0D1922EA26BDD96A00052649 /* ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D1922E926BDD96A00052649 /* ViewModel.swift */; };
0D1922ED26BDE0C600052649 /* AppRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D1922EC26BDE0C600052649 /* AppRouter.swift */; };
0D1922EF26BDE1A300052649 /* Services.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D1922EE26BDE1A300052649 /* Services.swift */; };
0D1922F226BDE29300052649 /* ZcashSDKStubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D1922F126BDE29300052649 /* ZcashSDKStubs.swift */; };
0D1922F426BDE5F200052649 /* MnemonicSeedPhraseHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D1922F326BDE5F200052649 /* MnemonicSeedPhraseHandling.swift */; };
0D1922F626BDE74500052649 /* KeyStoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D1922F526BDE74500052649 /* KeyStoring.swift */; };
0D1922F826BDEB3500052649 /* MockServices.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D1922F726BDEB3500052649 /* MockServices.swift */; };
0D4E7A0926B364170058B01E /* SecantApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D4E7A0826B364170058B01E /* SecantApp.swift */; };
0D4E7A0B26B364170058B01E /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D4E7A0A26B364170058B01E /* ContentView.swift */; };
0D4E7A0D26B364180058B01E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0D4E7A0C26B364180058B01E /* Assets.xcassets */; };
@ -33,6 +41,14 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
0D170A7126BC802800EB6A46 /* Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = "<group>"; };
0D1922E926BDD96A00052649 /* ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModel.swift; sourceTree = "<group>"; };
0D1922EC26BDE0C600052649 /* AppRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouter.swift; sourceTree = "<group>"; };
0D1922EE26BDE1A300052649 /* Services.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Services.swift; sourceTree = "<group>"; };
0D1922F126BDE29300052649 /* ZcashSDKStubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZcashSDKStubs.swift; sourceTree = "<group>"; };
0D1922F326BDE5F200052649 /* MnemonicSeedPhraseHandling.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MnemonicSeedPhraseHandling.swift; sourceTree = "<group>"; };
0D1922F526BDE74500052649 /* KeyStoring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyStoring.swift; sourceTree = "<group>"; };
0D1922F726BDEB3500052649 /* MockServices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockServices.swift; sourceTree = "<group>"; };
0D4E7A0526B364170058B01E /* secant.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = secant.app; sourceTree = BUILT_PRODUCTS_DIR; };
0D4E7A0826B364170058B01E /* SecantApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecantApp.swift; sourceTree = "<group>"; };
0D4E7A0A26B364170058B01E /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
@ -72,6 +88,49 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
0D170A7326BC802E00EB6A46 /* Routers */ = {
isa = PBXGroup;
children = (
0D1922EC26BDE0C600052649 /* AppRouter.swift */,
);
path = Routers;
sourceTree = "<group>";
};
0D170A7426BC9B7500EB6A46 /* Dependencies */ = {
isa = PBXGroup;
children = (
0D1922F326BDE5F200052649 /* MnemonicSeedPhraseHandling.swift */,
0D1922EE26BDE1A300052649 /* Services.swift */,
0D1922F526BDE74500052649 /* KeyStoring.swift */,
);
path = Dependencies;
sourceTree = "<group>";
};
0D1922E826BDD95000052649 /* Base */ = {
isa = PBXGroup;
children = (
0D170A7126BC802800EB6A46 /* Router.swift */,
0D1922E926BDD96A00052649 /* ViewModel.swift */,
);
path = Base;
sourceTree = "<group>";
};
0D1922EB26BDD9A500052649 /* Screens */ = {
isa = PBXGroup;
children = (
);
path = Screens;
sourceTree = "<group>";
};
0D1922F026BDE27D00052649 /* Stubs */ = {
isa = PBXGroup;
children = (
0D1922F126BDE29300052649 /* ZcashSDKStubs.swift */,
0D1922F726BDEB3500052649 /* MockServices.swift */,
);
path = Stubs;
sourceTree = "<group>";
};
0D4E79FC26B364170058B01E = {
isa = PBXGroup;
children = (
@ -95,11 +154,16 @@
0D4E7A0726B364170058B01E /* secant */ = {
isa = PBXGroup;
children = (
0D1922F026BDE27D00052649 /* Stubs */,
0D1922EB26BDD9A500052649 /* Screens */,
0D1922E826BDD95000052649 /* Base */,
0D170A7426BC9B7500EB6A46 /* Dependencies */,
0D4E7A0826B364170058B01E /* SecantApp.swift */,
0D4E7A0A26B364170058B01E /* ContentView.swift */,
0D4E7A0C26B364180058B01E /* Assets.xcassets */,
0D4E7A1126B364180058B01E /* Info.plist */,
0D4E7A0E26B364180058B01E /* Preview Content */,
0D170A7326BC802E00EB6A46 /* Routers */,
);
path = secant;
sourceTree = "<group>";
@ -259,7 +323,15 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0D1922ED26BDE0C600052649 /* AppRouter.swift in Sources */,
0D1922F226BDE29300052649 /* ZcashSDKStubs.swift in Sources */,
0D1922EF26BDE1A300052649 /* Services.swift in Sources */,
0D1922F826BDEB3500052649 /* MockServices.swift in Sources */,
0D4E7A0B26B364170058B01E /* ContentView.swift in Sources */,
0D1922F426BDE5F200052649 /* MnemonicSeedPhraseHandling.swift in Sources */,
0D170A7226BC802800EB6A46 /* Router.swift in Sources */,
0D1922F626BDE74500052649 /* KeyStoring.swift in Sources */,
0D1922EA26BDD96A00052649 /* ViewModel.swift in Sources */,
0D4E7A0926B364170058B01E /* SecantApp.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;

15
secant/Base/Router.swift Normal file
View File

@ -0,0 +1,15 @@
//
// Router.swift
// secant
//
// Created by Francisco Gindre on 8/5/21.
//
import Foundation
import SwiftUI
public protocol Router: ObservableObject {
associatedtype ViewOutput: View
func rootView() -> ViewOutput
}

View File

@ -0,0 +1,18 @@
//
// ViewModel.swift
// secant
//
// Created by Francisco Gindre on 8/6/21.
//
import Foundation
open class BaseViewModel<S> {
public var services: S
public init(services: S) {
self.services = services
}
}

View File

@ -0,0 +1,81 @@
//
// AppRouterRouter.swift
// secant
//
// Created by Francisco Gindre on 8/6/21.
//
import Foundation
import SwiftUI
enum AppRouterScreen {
case appLoading
case createRestoreWallet
case home
}
class AppRouter: Router {
// MARK: - Published vars
@Published var screen: AppRouterScreen = .appLoading
// MARK: - Private vars
// MARK: - Internal vars
var services: Services
// MARK: - Initialization
init(services: Services) {
self.services = services
}
// MARK: - Methods
@ViewBuilder func rootView() -> some View {
// Add your content here
NavigationView {
AppRouterView(router: self)
}
}
@ViewBuilder func createNew() -> some View {
Text("Create New")
}
@ViewBuilder func home() -> some View {
Text("Home Screen")
}
@ViewBuilder func loadingScreen() -> some View {
Text("Loading")
}
}
struct AppRouterView: View {
@StateObject var router: AppRouter
@ViewBuilder func viewForScreen(_ screen: AppRouterScreen) -> some View {
switch self.router.screen {
case .appLoading:
self.router.loadingScreen()
case .createRestoreWallet:
self.router.createNew()
case .home:
self.router.home()
}
}
var body: some View {
viewForScreen(router.screen)
.onAppear() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
if router.services.keyStorage.keysPresent {
router.screen = .home
} else {
router.screen = .createRestoreWallet
}
}
}
}
}

View File

@ -9,9 +9,11 @@ import SwiftUI
@main
struct SecantApp: App {
@StateObject var appRouter = AppRouter(services: MockAppServices())
var body: some Scene {
WindowGroup {
ContentView()
appRouter.rootView()
}
}
}

View File

@ -0,0 +1,114 @@
//
// MockServices.swift
// secant
//
// Created by Francisco Gindre on 8/6/21.
//
import Foundation
class MockAppServices: Services {
init(){}
var networkProvider: ZcashNetworkProvider {
MockNetworkProvider()
}
var seedHandler: MnemonicSeedPhraseHandling {
MockMnemonicPhraseHandling()
}
var keyStorage: KeyStoring {
MockKeyStoring()
}
}
class MockNetworkProvider: ZcashNetworkProvider {
func currentNetwork() -> ZcashNetwork {
ZcashMainnet()
}
}
class MockMnemonicPhraseHandling: MnemonicSeedPhraseHandling {
class TestSeed {
/**
test account: "still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread"
*/
let seedString = Data(base64Encoded: "9VDVOZZZOWWHpZtq1Ebridp3Qeux5C+HwiRR0g7Oi7HgnMs8Gfln83+/Q1NnvClcaSwM4ADFL1uZHxypEWlWXg==")!
func seed() -> [UInt8] {
[UInt8](seedString)
}
}
func randomMnemonic() throws -> String {
"still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread"
}
func randomMnemonicWords() throws -> [String] {
"still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread".components(separatedBy: " ")
}
func toSeed(mnemonic: String) throws -> [UInt8] {
TestSeed().seed()
}
func asWords(mnemonic: String) throws -> [String] {
"still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread".components(separatedBy: " ")
}
func isValid(mnemonic: String) throws {}
}
class MockKeyStoring: KeyStoring {
var birthday: BlockHeight?
var phrase: String?
func importBirthday(_ height: BlockHeight) throws {
guard birthday == nil else {
throw KeyStoringError.alreadyImported
}
birthday = height
}
func exportBirthday() throws -> BlockHeight {
guard let b = birthday else {
throw KeyStoringError.uninitializedWallet
}
return b
}
func importPhrase(bip39 phrase: String) throws {
guard self.phrase == nil else {
throw KeyStoringError.alreadyImported
}
self.phrase = phrase
}
func exportPhrase() throws -> String {
guard let p = self.phrase else {
throw KeyStoringError.uninitializedWallet
}
return p
}
var keysPresent: Bool {
return self.phrase != nil && self.birthday != nil
}
func nukePhrase() {
self.phrase = nil
}
func nukeBirthday() {
self.birthday = nil
}
func nukeWallet() {
nukePhrase()
nukeBirthday()
}
}

View File

@ -0,0 +1,242 @@
//
// ZcashSDKStubs.swift
// secant
//
// Created by Francisco Gindre on 8/6/21.
//
import Foundation
public typealias BlockHeight = Int
public protocol ZcashNetwork {
var networkType: NetworkType { get }
var constants: NetworkConstants.Type { get }
}
public enum NetworkType {
case mainnet
case testnet
var networkId: UInt32 {
switch self {
case .mainnet:
return 1
case .testnet:
return 0
}
}
}
extension NetworkType {
static func forChainName(_ chainame: String) -> NetworkType? {
switch chainame {
case "test":
return .testnet
case "main":
return .mainnet
default:
return nil
}
}
}
public class ZcashNetworkBuilder {
public static func network(for networkType: NetworkType) -> ZcashNetwork {
switch networkType {
case .mainnet:
return ZcashMainnet()
case .testnet:
return ZcashTestnet()
}
}
}
class ZcashTestnet: ZcashNetwork {
var networkType: NetworkType = .testnet
var constants: NetworkConstants.Type = ZcashSDKTestnetConstants.self
}
class ZcashMainnet: ZcashNetwork {
var networkType: NetworkType = .mainnet
var constants: NetworkConstants.Type = ZcashSDKMainnetConstants.self
}
/**
Constants of ZcashLightClientKit. this constants don't
*/
public class ZcashSDK {
/**
The number of zatoshi that equal 1 ZEC.
*/
public static var ZATOSHI_PER_ZEC: BlockHeight = 100_000_000
/**
The theoretical maximum number of blocks in a reorg, due to other bottlenecks in the protocol design.
*/
public static var MAX_REORG_SIZE = 100
/**
The amount of blocks ahead of the current height where new transactions are set to expire. This value is controlled
by the rust backend but it is helpful to know what it is set to and should be kept in sync.
*/
public static var EXPIRY_OFFSET = 20
//
// Defaults
//
/**
Default size of batches of blocks to request from the compact block service.
*/
public static var DEFAULT_BATCH_SIZE = 100
/**
Default amount of time, in in seconds, to poll for new blocks. Typically, this should be about half the average
block time.
*/
public static var DEFAULT_POLL_INTERVAL: TimeInterval = 20
/**
Default attempts at retrying.
*/
public static var DEFAULT_RETRIES: Int = 5
/**
The default maximum amount of time to wait during retry backoff intervals. Failed loops will never wait longer than
this before retyring.
*/
public static var DEFAULT_MAX_BACKOFF_INTERVAL: TimeInterval = 600
/**
Default number of blocks to rewind when a chain reorg is detected. This should be large enough to recover from the
reorg but smaller than the theoretical max reorg size of 100.
*/
public static var DEFAULT_REWIND_DISTANCE: Int = 10
/**
The number of blocks to allow before considering our data to be stale. This usually helps with what to do when
returning from the background and is exposed via the Synchronizer's isStale function.
*/
public static var DEFAULT_STALE_TOLERANCE: Int = 10
/**
Default Name for LibRustZcash data.db
*/
public static var DEFAULT_DATA_DB_NAME = "data.db"
/**
Default Name for Compact Block caches db
*/
public static var DEFAULT_CACHES_DB_NAME = "caches.db"
/**
Default name for pending transactions db
*/
public static var DEFAULT_PENDING_DB_NAME = "pending.db"
/**
File name for the sapling spend params
*/
public static var SPEND_PARAM_FILE_NAME = "sapling-spend.params"
/**
File name for the sapling output params
*/
public static var OUTPUT_PARAM_FILE_NAME = "sapling-output.params"
/**
The Url that is used by default in zcashd.
We'll want to make this externally configurable, rather than baking it into the SDK but
this will do for now, since we're using a cloudfront URL that already redirects.
*/
public static var CLOUD_PARAM_DIR_URL = "https://z.cash/downloads/"
}
public protocol NetworkConstants {
/**
The height of the first sapling block. When it comes to shielded transactions, we do not need to consider any blocks
prior to this height, at all.
*/
static var SAPLING_ACTIVATION_HEIGHT: BlockHeight { get }
/**
Default Name for LibRustZcash data.db
*/
static var DEFAULT_DATA_DB_NAME: String { get }
/**
Default Name for Compact Block caches db
*/
static var DEFAULT_CACHES_DB_NAME: String { get }
/**
Default name for pending transactions db
*/
static var DEFAULT_PENDING_DB_NAME: String { get }
static var DEFAULT_DB_NAME_PREFIX: String { get }
/**
fixed height where the SDK considers that the ZIP-321 was deployed. This is a workaround
for librustzcash not figuring out the tx fee from the tx itself.
*/
static var FEE_CHANGE_HEIGHT: BlockHeight { get }
static func defaultFee(for height: BlockHeight) -> Int64
}
public extension NetworkConstants {
static func defaultFee(for height: BlockHeight = BlockHeight.max) -> Int64 {
guard height >= FEE_CHANGE_HEIGHT else { return 10_000 }
return 1_000
}
}
public class ZcashSDKMainnetConstants: NetworkConstants {
private init() {}
/**
The height of the first sapling block. When it comes to shielded transactions, we do not need to consider any blocks
prior to this height, at all.
*/
public static var SAPLING_ACTIVATION_HEIGHT: BlockHeight = 419_200
/**
Default Name for LibRustZcash data.db
*/
public static var DEFAULT_DATA_DB_NAME = "data.db"
/**
Default Name for Compact Block caches db
*/
public static var DEFAULT_CACHES_DB_NAME = "caches.db"
/**
Default name for pending transactions db
*/
public static var DEFAULT_PENDING_DB_NAME = "pending.db"
public static var DEFAULT_DB_NAME_PREFIX = "ZcashSdk_mainnet_"
public static var FEE_CHANGE_HEIGHT: BlockHeight = 1_077_550
}
public class ZcashSDKTestnetConstants: NetworkConstants {
private init() {}
/**
The height of the first sapling block. When it comes to shielded transactions, we do not need to consider any blocks
prior to this height, at all.
*/
public static var SAPLING_ACTIVATION_HEIGHT: BlockHeight = 280_000
/**
Default Name for LibRustZcash data.db
*/
public static var DEFAULT_DATA_DB_NAME = "data.db"
/**
Default Name for Compact Block caches db
*/
public static var DEFAULT_CACHES_DB_NAME = "caches.db"
/**
Default name for pending transactions db
*/
public static var DEFAULT_PENDING_DB_NAME = "pending.db"
public static var DEFAULT_DB_NAME_PREFIX = "ZcashSdk_testnet_"
/**
Estimated height where wallets are supposed to change the fee
*/
public static var FEE_CHANGE_HEIGHT: BlockHeight = 1_028_500
}

1
symlink-templates.sh Executable file
View File

@ -0,0 +1 @@
ln -s $PWD/xctemplates ~/Library/Developer/Xcode/Templates

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -0,0 +1,17 @@
{
Kind = "Xcode.IDEFoundation.TextSubstitutionFileTemplateKind";
Platforms = (
"com.apple.platform.iphoneos",
"com.apple.platform.macosx",
);
Options = (
{
Description = "The name of the module to create";
Identifier = productName;
Name = Router;
Required = YES;
Type = text;
Default = Router;
},
);
}

View File

@ -0,0 +1,31 @@
//___FILEHEADER___
import Foundation
import SwiftUI
class ___FILEBASENAMEASIDENTIFIER___: Router {
// MARK: - Published vars
// Put published vars here
// MARK: - Private vars
// MARK: - Internal vars
var services: Services
// MARK: - Initialization
init(services: Services) {
self.services = services
}
// MARK: - Methods
func rootView() -> some View {
// Add your content here
NavigationView {
Text("Hello Word")
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@ -0,0 +1,16 @@
{
Kind = "Xcode.IDEFoundation.TextSubstitutionFileTemplateKind";
Platforms = (
"com.apple.platform.iphoneos",
);
Options = (
{
Description = "The name of the module to create";
Identifier = productName;
Name = Module;
Required = YES;
Type = text;
Default = Module;
},
);
}

View File

@ -0,0 +1,22 @@
//___FILEHEADER___
import SwiftUI
protocol ___FILEBASENAMEASIDENTIFIER___Router: AnyObject {
}
struct ___FILEBASENAMEASIDENTIFIER___: View {
@State var router: ___FILEBASENAMEASIDENTIFIER___Router?
@ObservedObject var viewModel: ___FILEBASENAME___ViewModel
var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
}
}
struct ___FILEBASENAMEASIDENTIFIER___Previews: PreviewProvider {
static var previews: some View {
___FILEBASENAMEASIDENTIFIER___(viewModel: ___FILEBASENAME___ViewModel(services: MockServices()))
}
}

View File

@ -0,0 +1,8 @@
//___FILEHEADER___
import Foundation
import Combine
class ___FILEBASENAMEASIDENTIFIER___: BaseViewModel<Services>, ObservableObject {
}