如何更改模擬器上的狀態列

這篇就來談談我是如何更改模擬器上的狀態列🎤

在 Xcode 11 Beta 3 以前,我是使用 SimulatorStatusMagic
而今天要弄截圖的時候發現,原來在 Xcode 11 Beta 4 之後,有內建的使用方法!

我喜歡讓 App store previews 上的時間顯示我自己的生日🎂算是一個小巧思(但沒人想知道)

而現在可以透過內建的指令來完成這件事,其中你可以使用下列這些

xcrun simctl status_bar

file

像是更改目前開著的模擬器時間:

xcrun simctl status_bar booted override --time "02:01"

成果圖:
file

而若是你的 Xcode 版本是 6 - 10 的話,就繼續使用 SimulatorStatusMagic 吧👌

SwiftUI + Google AdMob

這篇是一個簡單介紹 UIViewControllerRepresentable 的範例,
由於 Google AdMob 的 GADBannerView 不像上次提及的 UITextField 一樣,可以直接使用 UIViewRepresentable 來包裝;原因是它必須設置一個 rootViewController,也就意味著我們需要使用 UIViewControllerRepresentable 才能完成它。

Interfacing with UIKit

透過這個 Apple 官方的教學當中,我們可以從 UIPageViewController 的範例來做發想,故我的實作方式會是這樣:

import GoogleMobileAds
import SwiftUI
import UIKit

struct GADBannerViewController: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> UIViewController {
        let view = GADBannerView(adSize: kGADAdSizeBanner)
        let viewController = UIViewController()
        view.adUnitID = "your ad unit id in there."
        view.rootViewController = viewController
        viewController.view.addSubview(view)
        viewController.view.frame = CGRect(origin: .zero, size: kGADAdSizeBanner.size)
        view.load(GADRequest())
        return viewController
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}

如果需要處理 Delegate 的部分

你可以參閱上次 TextField 的處理方式,建構一個 Coordinator 來進行相關的作業👌

如果還有問題的話

歡迎直接透過各種方式找到我,一起來討論討論 SwiftUI 的應用👍
程式碼會同步到 GitHub 上,有任何想法都可以直接留言📒

對於 SwiftUI onDisappear 的誤解?

在接觸 SwiftUI 的這段時間以來,我一直在試著釐清一件事情,那就是

onDisappear 到底是不是壞的!

這件事情很玄,畢竟網路上大部分的資訊都告訴我們 onAppear 類似於 viewDidAppearonDisappear 類似於 ViewDidDisappear,然後再補上一句

Note: In the current SwiftUI beta onDisappear will never be called.
by HackingWithSwift

或是你可以看到在 StackOverFlow 上大家是這麼討論的

file

接著,在這一路以來,你又曾經碰過真的是 Apple 的 bug,所以你就會很理所當然地認為⋯⋯

沒錯,onDisappear 就是壞的!

直到認真找找官方文件到底有沒有使用到 onDisappear 的範例,於是找到了這篇 並下載下來研究發現

onDisappear 是會動的⋯

這代表著一件事,就是其實是我誤解它的使用方式,而非它是壞的。
來看看官方的這個 View

struct ProfileHost: View {
    @Environment(\.editMode) var mode
    @State var profile = Profile.default
    @State var draftProfile = Profile.default

    var body: some View {
        VStack(alignment: .leading, spacing: 20) {
            HStack {
                if self.mode?.wrappedValue == .active {
                    Button(action: {
                        self.profile = self.draftProfile
                        self.mode?.animation().wrappedValue = .inactive
                    }) {
                        Text("Done")
                    }
                }

                Spacer()

                EditButton()
            }
            if self.mode?.wrappedValue == .inactive {
                ProfileSummary(profile: profile)
            } else {
                ProfileEditor(profile: $draftProfile)
                    .onDisappear {
                        self.draftProfile = self.profile
                    }
            }
        }
        .padding()
    }
}

想了一下,若是我的話我會將 onDisappear 寫在哪裡?應該會是在 ProfileEditorbody 裡頭吧。而實際上測試了,在 ProfileEditor.body 裡頭實作

.onDisappear { print("disappear") }

是沒有效果的,我才得到一個結論

onDisappear 和 viewDidDisappear 不同

onDisappear 的概念是監聽你底下那個消失的動作

Adds an action to perform when this view disappears.

但並不是像 UIViewController.viewDidDisappear 一樣,是對物件本身消失去做動作。

因為大多數的文章都習慣將 onAppearviewDidAppear 做對比,也就造成我自己對於使用方式上產生誤解;實際上 onDisappear 的動作應該做在 superView 之中,而非直接寫在那個 struct 裡頭。

這大概就是一個從 UIKit 轉到 SwiftUI 才會誤解的地方了⋯⋯如果是一開始就從官方文件開始用 SwiftUI 學習寫 iOS 的人,應該不會陷入這種迷思🤷‍♂️

📒 SwiftUI + CoreData 的實戰心得🔥

SwiftUI + CoreData

file

這篇文章會紀錄我在目前的 side project 上,如何在 SwiftUI 下導入 CoreData;
而如同 在 SwiftUI 處理中文輸入法所會遇上的問題 所提及的,SwiftUI 身為一個還在測試階段的 framework,我們必須將當下的開發環境紀錄下來,以避免造成日後官方修正所造成的誤解。

開發環境

  • Xcode 11 Beta 7(但顯示為 Beta 6 - 11M392r )
  • macOS Catalina - 19A546d
  • iOS 13.1 Beta

使用情境

我要做一個貨幣的列表,並讓使用者可以對相對應的貨幣做隱藏與否,所以規格大概是需要一個 List,而 Row 裡頭呈現貨幣的名稱以及用 Toggle 來做控制隱藏的開關。

CoreData Model - Currency

file

Row 的部分

Xcode 11 Beta 5 之後NSManagedObject 可以視為一個 @ObservedObject,所以我們可以不必再弄一個 ManageRowModel,而是直接使用 NSManagedObject 來連動。
這邊我需要以英文大寫來顯示貨幣名稱以及一個控制是否顯示在主畫面上的開關。

struct ManageRow: View {
    @ObservedObject var currency: Currency

    var body: some View {
        HStack {
            VStack {
                Text(currency.name?.uppercased() ?? String())
                    .font(.title)
                    .fontWeight(.bold)
            }
            .padding()
            Toggle(isOn: $currency.isPresented) {
                Text(String())
            }
            .padding()
        }
    }
}

畫面如下方所呈現的樣式:

file

我們可以直接讓 Toggle(isOn: _)Currency.isPresented 連動,這樣便可以直接修改到相對應的值。

View 的部分

在 SwiftUI 裡頭有提供一個 @FetchRequest(fetchRequest: 的 propertyWrapper,而若要使用的話,記得要一併宣告 NSManagedObjectContext@Environment 之中,否則會報錯:

struct ManageView: View {
    @Environment(\.managedObjectContext) private var context
    @FetchRequest(fetchRequest: fetchRequest()) var currencies: FetchedResults

    var body: some View {
        List(currencies, rowContent: ManageRow.init)
    }

    static func fetchRequest() -> NSFetchRequest<Currency> {
        let request: NSFetchRequest = Currency.fetchRequest()
        request.sortDescriptors = [NSSortDescriptor(keyPath: \Currency.name, ascending: true)]
        return request
    }
}

這樣就可以在畫面建立時,透過 SwiftUI 的機制去執行 fetchRequest 並呈現出來。

⚠️ 注意事項

Identifible 的使用

若要在 List 之中直接使用 NSManagedObject 作為 RandomAccessCollection,可以讓它符合 Identifible

extension Currency: Identifible {}

但這個意味著我們會以預設的 id,如 NSManagedObject.objectId 作為是否需要重新繪製畫面的依據,而在這邊會遇上一個問題:
當我們將這個 List 作為另一個畫面的 Sheet 時,當它出現時,並不會重新繪製 List
也就是說已經在畫面上的 Row,會出現第一次畫面的狀態且之後 Sheet 出現都還會保持一樣的畫面。

簡單來說就是開關的狀態不會改變,除非使用者自己上下滑動觸動 SwiftUI 重新繪製的機制

所以我必須讓系統知道,isPresented 若有改變過的話,需要更新 row 的畫面

extension Currency: Identifiable {
    public var id: String { "\(String(describing: name)) \(isPresented)" }
}

這邊我目前的作法是將 id 包含了 isPresented 的狀態,所以當同一個 objectId 有不同的 isPresented 時,對於 List 是不同的 Row,這時就會重新繪製成正確的畫面。

@Environment(.managedObjectContext) 的使用

我這邊的使用情境是,使用者點擊一個設定的按鈕會跳出一個 ManageView,而 ManageView 可以點擊儲存或是直接滑掉放棄當前操作。
所以在前一個畫面上,我們需要為它設立 NSManagedObjectContext,並在 onDismiss 時捨棄掉這次的操作。

struct ContentView: View {
    ...
    var body: some View {
        ...
        .sheet(isPresented: $isManagePresented,
                   onDismiss: { CoreDataStack.shared.backgroundContext.rollback() },
                   content: manageView)
    }

    private func manageView() -> some View {
        ManageView().environment(\.managedObjectContext, CoreDataStack.shared.backgroundContext)
    }
}

以上便是目前在 SwiftUI 上實作 CoreData 的分享📒

SwiftUI 上的鍵盤處理方式

在 iOS 的開發過程之中,難免會碰到一個狀況,那便是 UITextField/ UITextView 被鍵盤所遮住了⌨️
在 UIKit 之下,多數人會使用套件來做全域的處理,如 IQKeyboardManager 就是一個十分經典的解決方案。

來說說 SwiftUI 上的鍵盤處理方式

在 SwiftUI 上,我們也可以很優雅地處理這一塊,如在 List 元件中,只需要分別監聽 UIWindow.keyboardWillShowNotificationUIWindow.keyboardWillHideNotification,以及加上個 .animation(.default) 來優化使用者體驗。

var body: some View {
        List(viewModel.rowModels, rowContent: DemoRow.init)
            .padding(EdgeInsets(top: 0, leading: 0, bottom: bottomPadding, trailing: 0))
            .onReceive(NotificationCenter.default.publisher(for: UIWindow.keyboardWillShowNotification),
                       perform: updateFrame)
            .onReceive(NotificationCenter.default.publisher(for: UIWindow.keyboardWillHideNotification),
                       perform: updateFrame)
            .animation(.default)
    }

完整的 struct 可以在 GitHub 上查看👍

成果動畫

有任何問題歡迎在底下留言👏有寫法上的建議可以直接在 GitHub 上反應👍
有想看看一些廢話的話則是可以在 Twitter 直接找到我喔!😂

Archie 的斜槓記錄 2019-08

這篇是什麼?

這篇文章的內容並不會圍繞 iOS 或是 Swift 等,而是投資理財的部分,其中並不會有什麼教學或是分析,單純只是我這個月的損益總結而已。

2019 年 8 月份總結

Adsense

其實我一直沒有在看這方面的收入,不過看來要加把勁在廣告上才是💪
大多數都是從一、兩個我沒有在維護的 App 來的⋯⋯
九月份會開始掛上網站的廣告和看能不能在 iOS 13 正式版上線的時候,
將一個由 SwiftUI + Combine 所編寫的 side project 弄上架💦

file

Bitfinex

我有在 Bitfinex 搭配 Coinlend 進行放貸,並在 8/9 加碼了 $ 50,000 元進去
八月份的報表大概如下
file
共獲利了 USD $ 31.68

台股股利

這個月領了三次股利,共計 $ 1,596 元

八月份總收入

  • Adsense USD $ 1.33
  • Bitfinex USD $ 31.68
  • 台股股利 $ 1,596 元

共計 $ 2,629.87 元

斜槓率

這是我自己給的定義,簡單來說就是當每個月的額外收入達 $ 50,000 元;
每年額外收入達 $ 600,000 元時,就是可以辭去正職工作的時候(不知道要到什麼時候⋯⋯)

2,629.87 / 50,000 = 5.25974 %

[|_________ 5% __________]

再繼續努力,九月底再來記錄吧!

在 SwiftUI 處理中文輸入法所會遇上的問題

最近開始在嘗試把玩 SwiftUI 並打算作為一個 side project 的主要 UI 編寫方式,這篇文章則是其中一個使用情境所遇上的困難處。

開發環境

  • Xcode 11 Beta 7(但顯示為 Beta 6 - 11M392r )
  • macOS Catalina - 19A546d
  • iOS 13.1 Beta

記錄這點蠻重要的,因為可能過個幾版這篇文章就沒參考價值了😂
還沒有下載更新的朋友,可以快去更新一下!

使用情境

我要來做一個搜尋的功能,讓使用者可以輸入關鍵字,並自動搜尋完後將結果呈現在下方👏

於是我們可以用兩個 UI 元件來達成這件事

  • TextField
  • List
    並寫一個 @Binding var 或是 @ObservedObject var 來讓 TextField 的 text 有個 binding 的地方,當它的值有更新時,觸發搜尋的動作🚀
    而搜尋完的結構再來更新畫面上的 List

聽起來沒什麼毛病,對吧?

實際上你會遇上的問題⋯

當你想和我一樣,用拼音的輸入方式時;不論是你拼音還是注音,當你按下第一個音時,便會跑一次上述的流程了⋯⋯
而我們所期望的流程應該是使用者選完字後再進行搜尋,這點在目前的 TextField 是做不到的,因為它不會判別目前是否有還沒拼完音的狀況就發動了。

那該怎麼辦呢?

喵神這麼說

你可以查看一下當時的相關推文

我的作法就是以 UIViewRepresentable 來包裝,詳細的程式碼可以到 GitHub 查看👍

成果

當輸入了 luo dong 但是還沒選字的話並不會有動作。


而當選字後便會進行搜尋🔍

如果還有什麼問題的話,歡迎留言討論👏

建立 Xcode 的檔案範本

什麼是範本?

在新增檔案的時候,會有預設的這幾種類型可以選擇;而當我們有一些自己常用的架構,如 Coordinator、ViewModel 等,可以自建一個範本來使用。
下面舉例幾個預設的 Swift 檔案

Swift File

位置在
/Applications/Xcode.app/Contents/Developer/Library/Xcode/Templates/File Templates/Source/Swift File.xctemplate

//___FILEHEADER___

import Foundation

SwiftUI View

位置在
/Applications/Xcode-beta.app/Contents/Developer/Library/Xcode/ExtraTemplates/File Templates/User Interface

//___FILEHEADER___

import SwiftUI

struct ___FILEBASENAMEASIDENTIFIER___ : View {
    var body: some View {
        Text(/*@START_MENU_TOKEN@*/"Hello World!"/*@END_MENU_TOKEN@*/)
    }
}

#if DEBUG
struct ___FILEBASENAMEASIDENTIFIER____Previews : PreviewProvider {
    static var previews: some View {
        ___FILEBASENAMEASIDENTIFIER___()
    }
}
#endif

這些是跟著 Xcode.app 走的,而若是我們使用者自定義要使用的,得放在
/Users/UserName/Library/Developer/Xcode/Templates
先建立一個資料夾,或是可以到上述的位置複製一份過來。
需要至少三個檔案

  • FILEBASENAME.swift
  • TemplateIcon.png
  • TemplateInfo.plist

我自己的使用方式是複製系統的 Swift file 來做修改 🔧
也比較好去熟悉可替換文字和 header 的使用方式 👍

Get ETH balance and tokens balance

前言

最近都在處理區塊鏈相關的問題,其中一個就是怎麼直接透過節點(****.infura.io)來取得 ETH 和其他 ERC-20 的地址餘額(balance)。

JSON RPC

ETH 節點所使用的 request body 是依照 JSON RPC 的方式,也就是我們所使用的 request path 都是相同的,像是主要的 https://mainnet.infura.io ;不同的是,我們可以透過不同的 body 內容來區別不同的需求。

基本的 body 格式

    "jsonrpc": "2.0",
    "method": "method name",
    "params": [],
    "id": 1
}

取得 ETH 餘額

取得 ETH 餘額的方式較為單純,節點有提供一個 method 是 eth_getBalance,使用起來如下

{
    "jsonrpc": "2.0",
    "method": "eth_getBalance",
    "params": ["你的 ETH 地址", "latest"],
    "id": 1
}

其中這邊的 latest 意思是指向最新的那個區塊取資料的意思;
我們便可以得到 response:

{
    "id":1,
    "jsonrpc": "2.0",
    "result": "0x0234c8a3397aab58" // 158972490234375000
}

其中注意到一點是,result 所回應的是你這個地址有幾聰的 hex 字串,所以當我們取得的時候得注意一下。

ERC-20 的餘額

而 ERC-20 的餘額就沒有上述那麼直覺的取得方式了,我們需要透過別的 method 來完成這個動作。
這邊我們只是需要取得餘額,而沒有更新合約的狀態,所以使用 eth_call 這個 method。

{
    "jsonrpc": "2.0",
    "method": "eth_getBalance",
    "params": [
    {
        "to": "Token 的 contract address",
        "data": "data"
    },
    "latest"
    ],
    "id": 1
}

這邊的重點便是 data 的部分,它的組成為
0x + 8 bits + 64 bits
其中 8 bits 是由 function signature hash 的結果取前 8 bits;
我們這邊所使用的 function 名稱是 balanceOf(address),去做 Keccak-256(SHA-3)hash 可以得到
70a08231b98ef4ca268c9cc3f6b4590e4bfec28280db06bb5d45e689f2a360be
而前 8 bits 便是 70a08231
後 64 bits 則是所要查詢的 ETH 地址,
000000000000000000000000 + 要查詢的地址 40 bits
所以組成起來的 data 欄位就會是:
0x70a08231000000000000000000000000 + ETH 地址
這樣就可以取得相對應的 token 餘額了。

BIP-39 Mnemonic validate

前言

最近處理了一些加密貨幣的問題,其中一個是助記詞的驗證,這邊將會解釋助記詞的生成和驗證方式。

生成助記詞

首先我們這邊所介紹的是助記詞生成方式,先建立一個 128 bytes 的隨機序列,也就是隨機產生 16 個 UInt8 的序列;
主要有分成 128、160、192、224、256 bits(每 32 bits 做為一個區間),而下列會以 128 bits 作為流程解釋。

對隨機序列加密(SHA256)

我們對剛剛所產生的序列做 SHA256 加密,便會得到由 32 個 UInt8 所組成的 Array。

Checksum

用來驗證助記詞是否正確的方式是透過 Checksum 來辨別,而 Checksum 的 size 為序列的長度 / 區間,如我們這邊所提及的 128 / 32 = 4;
意思便是剛剛所加密完的序列,我們取前面 4 bytes 的數值當作 checksum。

產生助記詞

而剛剛的隨機序列(128 bits)加上 4 bits 的 checksum 組成 132 bits,接著我們每 11 bits 作為一個分隔,也就可以得到 132 / 11 = 12 個數字。
而每 11 bits 作為一個分隔的意思也意味著數字的區間落在 0 - 2047 之間,也就是為什麼 12 字的助記詞所支援的單字庫數量為 2048。
接著就到詞庫裏頭撿取相對應 index 的字詞來組成助記詞。

驗證助記詞

驗證的方式就是將上述的流程反過來做,我們先講助記詞轉成詞庫的 index 順序;
接著看最後 4 bits 的內容,也就是這組助記詞的 checksum。
而前 128 bits 以 8 bits 作分隔,接著以 SHA256 加密,判斷前 4 bits 是否和剛剛提及的 checksum 相符。

Bitnami