Archie 的斜槓記錄 2019-10

這個月 Archie 的斜槓記錄又來啦!

AdSense

少少的 US$1.41

file

Bitfinex

這個月的年利率大約都落在 7 - 10 % 之間,不過最近因為習近平的一句話,有幾天上漲到 15 % 左右,但沒過多久就回歸到 9 % 的位置

file

所以這個月每日的利息走勢圖如下,多半時間是低於一塊美金的:
file

而這個月的收益為

file

台股獲利

這個月買賣股票加上屁偉(我弟弟)抽中上海商銀直接賣掉的分紅,一共獲利 $10,760 元。

總收入

美金持續下滑到 30.54 左右,所以 Bitfinex 那邊依然是被匯差吃死死⋯⋯😭

  • AdSense US$1.41 -> $43 元
  • Bitfinex US$24.68 -> $754.47 元
  • 台股獲利 $$10,759 元

這個月的斜槓收入為
$$11551.45 / 50,000 = 23.10%

[||||______ 23% __________]

目前看起來 AdSense 那一塊真的太少了😂需要好好思考一下怎麼增加那一部分的收益🤔️
而 11 月還會領到元大高股息 0056 的現金股利,希望台股獲利可以穩定👌

Checklist template

Checklist Template 是什麼分類?

這是一個新的分類,不侷限於 iOS 或是程式開發的層面,對於我而言是一個新的領域;這有點像是檢查清單必做清單或是 SOP 等概念,簡單舉幾個例子

雙 NP 流程

  • 確認原電信合約
  • 和原電信商解約
  • 攜碼至另一家電信的預付卡
  • 至通訊行辦理攜碼方案
  • 等預付卡開通,使用其卡片
  • 等原電信商開通

Cocoapods binary 流程

file

  • gem install cocoapods-binary
  • 在 Podfile 最上方加入 plugin 'cocoapods-binary'all_binary!
  • pod install

阿齊推薦的宜蘭十個小吃

  • 阿德魯肉飯
  • ⋯⋯

諸如此類的,這些流程它可以是一個個人的 SOP 或是有點像是小教學,而這當然只是一個我自己的範本,你可以刪刪減減成自己的版本👌

十月份先以這個分類為測試,若流量大一些的話,再擴大這個專案吧!

Archie 的斜槓記錄 2019-09

這個月 Archie 的斜槓記錄又來啦!

AdSense

廣告部分,九月份上架了兩個 App;
一個是 iOS 13+ 的匯率計算機、一個是 Flip clock,而流量都還是十分低的狀態,不過金額比上個月多了 US$ 1.2,來到了 US$ 2.53 。

file

Bitfinex

而九月份的放貸收入為 US$ 30.33,受到了虛擬貨幣價格走低的影響,這部分是比上個月來得少的,少了 US$ 1.35。

file

台股獲利

這個月領了合作金庫的股利 4,653 元,以及買賣其他股票小賺了 985 元。

總收入

加上目前美金匯率也降到了 31,使得總獲利其實沒什麼成長。

  • AdSense US$ 2.53 ➡️ $78.43
  • Bitfinex US$ 30.33 ➡️ $940.23
  • 台股獲利 $5,548
  • 額外收入 $1,500

這個月斜槓收入為 $8,066.55

8,066.55 / 50,000 = 16.1331 %

[|||_______ 16% __________]

不過十月份印象中沒有股利可領,下一篇大概會少上許多吧😭

雙 NP:中華電信➡️台灣大哥大➡️中華電信 v2

繼上次雙 NP 之後,合約又來到了即將到期的時候,而今日因昨晚發燒請了病假,中午狀況好轉後便順道處理了合約的問題。

這次打算申辦的是

file

每月費用大約如下
699 - 7,000 / 30 = 466 元

大地遊戲開始

2019-09-27 12:56 中華電信

由於我的合約到期日為 10/03,故要先到中華電信進行提前解約並繳納提前解約終端設備補貼款(我的部分是 106 元)

2019-09-27 13:01 台灣大哥大直營門市

告知從中華電信攜碼轉預付卡,而將於 2019/9/28 開通台灣大哥大門號(可以接電話、沒網路)

2019-09-27 13:10 地標網通

帶著前面的收據和申請書來申請,而中華電信的合約內容比較多,所以簽署了比較多份文件。
將於 2019/9/29 恢復成中華電信門號🎉

全部在羅東跑完大概半小時內可以處理完畢,再來就是等各家電信開通服務即可!

其他方案可以到地標網通查詢,或是其他通訊行做詢問。

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

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

在 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 直接找到我喔!😂

Bitnami