如何在 Swift project 底下加入 Bridging-Header File

Step1. 首先在 project 底下建立新 File (File -> New File -> Header File),命名為 YourApp-Bridging-Header

Step2. 進入到 project 選取 app target -> Build Settings -> search bridging header 出現下面這樣,在紅色區塊填入:

$(SRCROOT)/$(PROJECT_NAME)/$(SWIFT_MODULE_NAME)-Bridging-Header.h

(注意如果不是放在與 project 同一層 Directory 請在中間加入 path 如下:

$(SRCROOT)/$(PROJECT_NAME)/HeaderFilePath/$(SWIFT_MODULE_NAME)-Bridging-Header.h

以上就大功告成囉!

[MAC版] 新手教學,輕鬆用 wordpress.org 免費架設自己的網頁 (只需購買網域費用)

整體分成 3 個步驟

  1. 申請網域
  2. 申請主機與建立資料庫
  3. 安裝 wordpress.org
  1. 步驟一:申請網域
    1. 找到一個適合自己的網域名稱
      1. 網域名稱命名方式:
        1. 名字簡短 (3個單字以下的組合為佳)
        2. 文字有意義,容易記容易讀
        3. 使用到搜尋關鍵字 (travel、foodie、money 等)
        4. 如果要使用主打自創品牌,也可以使用自創詞,但不建議一般部落客使用自創詞
        5. (請注意避免) 使用底線、數字等等,容易讓名字看起來像釣魚網站而不受信任
        6. 如果想不到好的名字,或者喜歡的名稱都被取走了,可以使用命名網頁來找靈感唷!推薦使用 nameboy: (https://www.nameboy.com/)
      2. 後綴:
        1. 一般國際性網站使用 .com 居多,不知道用什麼就用 .com 啦!
        2. 主要以針對台灣用戶則可選擇 .tw or .com.tw 為後綴
  1. 取好網域名稱後,我們可以到 GoDaddy (https://tw.godaddy.com/) 來購買自己的網域名稱
  1. 步驟二:申請主機與建立資料庫
    1. 有了網域名稱後,我們也需要一個主機來 host 我們的網站,這裡推薦的是 FREEHOSTRING (https://www.freehosting.com/),可以使用免費的方案來申請主機
    2. FREEHOSTING 免費版規格:
      1. 可支援 1 個網站
      2. 可使用 1 個資料庫
      3. 使用空間 10G
      4. 無流量限制
    3. 申請好了之後約等待 30 分鐘至 1 小時後我們會收到一則 Email 裡面包含了後台的網址:
    4. 接著我們使用信件中給的網址與名稱、密碼進到控制台,如果還沒創建 Database 會有出現新增,可以很直覺的直接新增我們的 Database,以下附圖是已經建立好 Database 了!建立好 Database 會有剛剛申請的 User 與 password 可以先複製或截圖下來,等等安裝 wordpress.org 會使用到
      1. 建立好 Database 會有剛剛申請的 User 與 password 可以先複製或截圖下來,等等 wordpress.org 會使用到
      2. 再來我們回到 GoDaddy 網站中,從右上角 My Product -> My Domain -> DNS manage -> 可以找到頁面中間有一欄可以更改 Nameservers,將原本的 Nameservers 改為信件中給的 Domain configuration,注意它需要一段時間更新,大概等個 10 分鐘刷新一下再進來確認有沒有更新
      3. 完成後這個步驟就算完成
  1. 步驟三:開始架設網站
    1. 有了網域和主機資料庫之後我們就可以進到進到 wordpress.org 註冊網站了 (注意不是 wordpress.com!它要錢!)
    2. 進到 WordPress.org
      1. 首先註冊帳號
      2. 在首頁可以下載一包安裝檔,按下 Get WordPress -> Download
      3. 下載完解壓縮後,我們打開 wordpress directory 打開裡面的一個 wp-config-sample.php 利用文字編輯器將它打開,然後把剛剛 Database 資料填入,只要改上面 3 個設訂(包括 database 名稱、使用者名稱、密碼),localhost 等都不需要改動。改好之後把檔名改存成 wp-config.php
      4. 之後我們可以先把 wordpress directory 拉到桌面。
      5. 接下來我們要去安裝一個遠端傳輸的工具:FileZilla (https://filezilla-project.org/download.php?type=client)
      6. 打開 FileZilla 將剛剛 FreeHosting 寄來 Email 裡的 FTP client configuration (包括 hosting、username、password) 打在最上方進行連線
      7. 連線好之後,我們找到遠端 public_html directory,將裡面的 files 先全部刪掉 (包含 index.html、cgi bin dir、400.shtml 等等),然後我們將 wordpress 裡的全部東西丟到 public_html 下面,切記是上傳裡面的東西就好,不要包含 wordpress 資料夾,上傳好的結果圖如下:
      8. 接著我們進到我們的 domain,就會看到開始安裝 wordpress 的程序,只要出現歡迎來到 wordpress 五分鐘安裝步驟!就可以開始設定使用者密碼等,設定完成後就可以到我們申請的 domain 來看到我們的網站囉!(本步驟無截圖,但就照一般正常安裝即可)

[Solved] iOS issue: WebView for FB login problem

Today, I’m going to share a problem we encountered recently. When we use WKWebView in the App to enter the web version of the Facebook page, we will not be able to log in. We got an error message as follows:

For your account security logging into Facebook from an embedded browser is disabled.

However, all the information on the Internet only mentions Android. Why can’t I log in to iOS?But I found that some of my projects are able to log in. After researching for a while, I discovered that you must add microphone permissions or camera permissions into info.plist and then you can log in facebook like magic!

Privacy - Microphone Usage Description
Privacy - Camera Usage Description

SwiftUI: 使用 WKWebView

今天要來利用 WKWebView 讀取網頁,首先要運用 UIView 時我們可以使用 UIViewRepresentable 來將其包裝成 SwiftUI 可以使用的形式。我們首先建立一個符合 UIViewRepresentable 協定如下:

struct SwiftUIWebView: UIViewRepresentable {
    
    func updateUIView(_ uiView: WKWebView, context: Context) {

    }
    
    func makeUIView(context: Context) -> WKWebView {
        
    }
}

在 makeUIView 中我們直接回傳一個 WKWebView 的實例(instance),在 updateUIView 中,我們實作 loading URL :

struct SwiftUIWebView: UIViewRepresentable {
    
    let urlString: String?
    
    func updateUIView(_ uiView: WKWebView, context: Context) {
        guard let webURL = URL(string: urlString ?? "") else {
            return
        }
        let request = URLRequest(url: webURL)
        uiView.load(request)
    }
    
    func makeUIView(context: Context) -> WKWebView {
        return WKWebView(frame: .zero)
    }
}

建立好 SwiftUIWebView 後,我們就可以直接在 SwiftUI 中的 ContentView 來做使用:

struct ContentView: View {
    
    var body: some View {
        SwiftUIWebView(urlString: "https://google.com")
    }
}

為了讓我們的 code 更加彈性,我們將網址另外移出為獨立變數,並將 SwiftUIWebView 改為 @Binding 變數,就大功告成啦!

struct ContentView: View {
    
    @State var urlString = "https://google.com"
    var body: some View {
        SwiftUIWebView(urlString: $urlString)
    }
}

struct SwiftUIWebView: UIViewRepresentable {
    
    @Binding var urlString: String
    
    func updateUIView(_ uiView: WKWebView, context: Context) {
        ...
    }
    
    func makeUIView(context: Context) -> WKWebView {
        ...
    }
}

完整 code 下載

SwiftUI: Onboarding 設計 (一)

今天要使用 SwiftUI 簡單設計一款 App Onboarding:

首先我們要先在 code 中引進 TabView 如下:其中的 .tabViewStyle 我們選擇的是 PageTabViewStyle 是讓 View 有像 Page 般滑動的效果,indexDisplayMode = .always 是顯示下方點點,indexViewStyle 中我們家上了 backgroundDisplayMode = .always 是呈現點點的半透明背景:

struct ContentView: View {
    
    var body: some View {
        TabView{
            Text("Tab 1")
            Text("Tab 2")
            Text("Tab 3")
        }
        .tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))
        .indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
    }
}

接者我們定義 OnboardingData:這個資料會是等等我們要呈現在 OnboardingView 中的資料,其中的圖片,我們可以直接下載範例圖片 plan.jpgmoney.jpgenjoy.jpg。(以上圖片皆取自於 freepik)

struct OnboardingData: Hashable, Identifiable {
    
    let id: Int
    let mainImage: String
    let titleText: String
    let subtitleText: String

    static let list: [OnboardingData] = [
        OnboardingData(id: 0, mainImage: "plan", titleText: "Select your destination", subtitleText: "Choose your destination and plan your trip"),
        OnboardingData(id: 1, mainImage: "money", titleText: "Book your ticket", subtitleText: "Use the price comparison system to book tickets and restaurants with the lowest prices"),
        OnboardingData(id: 2, mainImage: "enjoy", titleText: "Enjoy the trip", subtitleText: "Please start and enjoy your journey!")
    ]
}

接著我們定義 OnboardingView,也就是我們的呈現頁面:

struct OnboardingView: View {
    
    var data: OnboardingData
    
    var body: some View {
        ZStack(alignment: .bottom) {
            VStack(spacing: 20) {
                ZStack {
                    Image(data.mainImage)
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(width: 300, height: 300)
                        .offset(x: 0, y: 120)
                }
                
                Spacer()
                
                Text(data.titleText)
                    .font(.title2)
                    .bold()
                    .foregroundColor(Color.black)
                
                Text(data.subtitleText)
                    .font(.headline)
                    .multilineTextAlignment(.center)
                    .frame(maxWidth: 250)
                    .foregroundColor(Color.gray)
                
                Spacer()
            }
        }
    }
}

並將其利用 ForEach 套入我們的 ContentView 之中,currentTab 代表的是目前正在顯示的 page:

struct ContentView: View {
    
    @State private var currentTab = 0
    
    var body: some View {
        TabView(selection: $currentTab) {
            ForEach(OnboardingData.list) { viewData in
                VStack {
                    OnboardingView(data: viewData)
                        .tag(viewData.id)
                }
            }
        }
        .tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))
        .indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
    }
}

目前我們已經大致完成 Onboarding 了!為了將 Onboarding 呈現的更加完美,我們將在前兩頁加入 Skip 與 Next 按鈕,在最後一頁加入 Start 按鈕,因此我們將剛剛的 OnboardingView 裡 VStack 中的最下面再加入以下程式碼:

struct OnboardingView: View {

    var data: OnboardingData

    var body: some View {
        ZStack(alignment: .bottom) {
            VStack(spacing: 20) {
                    
                ...

                if data.id == 2 {
                    Button {

                    } label: {
                        ZStack {
                            Rectangle()
                                .foregroundColor(Color.blue)
                                .frame(width: UIScreen.main.bounds.size.width, height: 80)
                            Text("Get Started")
                                .font(.headline)
                                .foregroundColor(.white)
                        }
                    }
                } else {
                    HStack {
                        Button {

                        } label: {
                            Text("Skip")
                                .font(.headline)
                                .foregroundColor(.gray)
                        }
                        .padding(.leading, 40)
                        .padding(.bottom, 16)
                        Spacer()
                        Button {

                        } label: {
                            Text("Next")
                                .font(.headline)
                                .foregroundColor(.blue)
                        }
                        .padding(.trailing, 40)
                        .padding(.bottom, 16)
                    }
                }
            }
        }
    }
}

完成之後我們會發現在第 3 頁同時有 Start 按鈕又有 index 疊加在上頭,而這並不是我們想要呈現的,我們還需稍微修正我們的 ContentView:

struct ContentView: View {
    
    @State private var currentTab = 0
    
    var body: some View {
        TabView(selection: $currentTab) {
            ForEach(OnboardingData.list) { viewData in
                VStack {
                    OnboardingView(data: viewData)
                        .tag(viewData.id)
                }
            }
        }
        .tabViewStyle(PageTabViewStyle(indexDisplayMode:  currentTab == 2 ? .never : .always))
        .indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: currentTab == 2 ? .never : .always))
    }
}

如此一來就差不多大功告成了!但等等,在瀏海機上面,Get Started 按鈕並不如預期在最底下,因此我們需要利用 edgesIgnoringSafeArea(.bottom) 來稍作調整:注意 VStack 與 TabView 都需加上。

struct ContentView: View {

    @State private var currentTab = 0

    var body: some View {
        TabView(selection: $currentTab,
                content:  {
            ForEach(OnboardingData.list) { viewData in
                VStack {
                    OnboardingView(data: viewData)
                        .tag(viewData.id)
                }
                .edgesIgnoringSafeArea(.bottom)
            }
        })
            .tabViewStyle(PageTabViewStyle(indexDisplayMode:  currentTab == 2 ? .never : .always))
            .indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: currentTab == 2 ? .never : .always))
            .edgesIgnoringSafeArea(.bottom)
    }
}

完整  code 下載


參考教學與圖片資源:

複利效應 書評

推薦度:🌕🌕🌕🌕🌕

什麼人適合閱讀? 想不斷成長,培養好習慣、改掉壞習慣卻又不知如何開始以及如何堅持的人

可以學習到什麼:

  • 達成目標的方法
  • 培養好習慣與改掉換壞習慣的方法
  • 保持動力的方法
  • 了解當下的選擇如何成就未來的自己
  • 更加瞭解自己的目標以及未來想成為什麼樣的人

其實這本書與原子習慣想要表達的概念十分相近,也就是你的每個極微小的選擇都會累積成為一個不可思議的力量。

可以從書中看出作者是對自身要求極高的人,彷彿完美的利用他人生中的每分每秒去做學習。

那複利效應是什麼呢?書中用簡單的一個公式來說明:

明智的小小選擇 + 持之以恆 + 時間 = 大不同

眼前的一個微不足道的選擇,重複了一千一萬遍,造成的結果就是複利效應的可觀之處,而這些所謂的重複的選擇就是習慣,好習慣在時間的堆積下會造成可觀的成果,相反的,壞習慣也會導致可怕的結果。

針對如何掉壞習慣與培養好習慣,作者用了一個章節來做討論,我只從裡頭挑選幾個較為特別的概念。首先是加法 > 減法,比方說,當我們要減少甜食攝取,可以其他較為簡康的東西取代,但這時候我們要告訴自己,我能吃到更多營養且好吃的蔬果,而不是我不能吃甜食了,說服自己的大腦你並沒有被剝奪什麼,反而是獲得了什麼。另外,當我們達成一個階段性目標時,也要適時給予自己獎勵。

再來是如何保持動力,如果每天照表操課,不用多久,肯定覺得生活漸漸變得枯燥乏味,所以我們可以在固定的時間,比如每週、每月、每年來適當的訂定不同的目標。除次之外,一天的早晚往往是最好安排習慣行為的最佳時機,因此可以把想培養的小習慣按排在起床時或者是睡覺前。作者以他自身為例,他也鼓勵讀者們可以在睡前閱讀一些激勵人心的語錄來餵養自己的大腦,這就是在下一章節所聚焦的課題。

當你給自己的大腦餵食垃圾食物,他就會吸收他,導致大腦充斥著這些髒東西來阻礙我們的創造力,因此我們要學習控制自己所接觸的媒體影視,也要慎選我們所交往的對象。

最後一章節也是我最愛的一章節,也是極少在其他書中讀到的概念,說起來簡單,做起來也不難,但卻能讓我們的事半功倍,只要在每次設定目標完成時再多做一點努力,就可以在複利效應的作用下事半功倍,比方說每天設定跑步 30 分鐘,在到達 30 分鐘時多跑 3 分鐘,就能讓累積下可觀的的成果。

至於要再花多少努力去達到你的目標呢?首先當然要先知道自己有多麼渴望那個目標,如果真的非常渴望它,就要做出超過別人預期的努力去達成它,書中舉出他朋友要去面試的例子,來找作者諮詢,在選拔前不需要到現場,只有在最後篩選出的三名才需要到現場,但作者鼓勵他直接去現場,並且試圖搜尋所有面試者的資訊,投其所好,甚至買禮物送他們,雖然我也覺得有點過於激進,但我認為作者想表達的就是積極到不能再積極、好到不能再好、不能再突出,最後你才會顯得你與眾不同。

最後作者也在附錄中提供一些表格,讓讀者實作起來更簡單。

SwiftUI: 讀取遠端圖片 (Part2. 使用 SDWebImageSwiftUI)

除了自己 coding 之外,也有一個非常方便使用的套件 SDWebImage。本章就來直接介紹他的使用方法:

首先安裝方式,我們可以在 podfile 裡面加入以下指令:

pod 'SDWebImageSwiftUI'

接著在上方 import SDWebImageSwiftUI,就可以使用 WebImage 指令讀取遠端圖片了:

import SDWebImageSwiftUI

struct ContentView: View {
    var body: some View {
        WebImage(url: URL(string: "https://i.imgur.com/SbqZHV2.jpg"))
        .resizable()
        .frame(width: 200.0, height: 200.0)
        .aspectRatio(contentMode: .fit)
    }
}

也可以使用 placeholder 指令來定義當遠端圖片正在讀取時所顯示的圖片:

var body: some View {
    WebImage(url: URL(string: "https://i.imgur.com/SbqZHV2.jpg"))
    .resizable()
    .placeholder {
        Image(systemName: "photo")
            .resizable()
            .frame(width: 160.0, height: 160.0)
            .foregroundColor(.gray)
            .opacity(0.3)
    }
    .frame(width: 200.0, height: 200.0)
    .aspectRatio(contentMode: .fit)
}

除此之外也可以使用 WebImage 直接讀取遠端的 .gif 檔案,非常方便好用。可以使用 .playbackRate 控制播放速度:

struct ContentView: View {
    
    @State var isAnimating: Bool = true
    let gifImageName = "https://media.giphy.com/media/SvFFU6fK9YNQkoEe0Z/giphy.gif"
    
    var body: some View {
        WebImage(url: URL(string: gifImageName), isAnimating: $isAnimating)
            .playbackRate(2.0) // Playback speed rate
    }
}

還可以使用 .customLoopCount 來決定反覆播放的次數,無指定就是無限循環:

var body: some View {
    WebImage(url: URL(string: gifImageName), isAnimating: $isAnimating)
        .customLoopCount(1) // Custom loop count
        .playbackRate(2.0) // Playback speed rate
}

詳細其他功能與介紹可以直接到 Github 網站查看:https://github.com/SDWebImage/SDWebImageSwiftUI

SwiftUI: 讀取遠端圖片 (Part1. 自己 code)

這個單元要來介紹如何用 SwiftUI 讀取遠端圖片。

首先我們需要建立一個從遠端讀取 Image URL 的類別,這個類別繼承 ObservableObject ,當 URL 一被讀取成功時,就會通知有訂閱此 ObservableObject 的物件,程式碼如下。

在這裡的 PassthroughSubject 可以直接從字面意思去理解,它沒有初始值,當它一被指定值時,它會通知訂閱者對此作出相對應的改變。此處的使用方式就是當 data 一被設定時,didChange 會利用 .send 指令,通知他的訂閱者作出改動。在 Apple 文件的定義如下:

As a concrete implementation of Subject, the PassthroughSubject provides a convenient way to adapt existing imperative code to the Combine model.Unlike CurrentValueSubject, a PassthroughSubject doesn’t have an initial value or a buffer of the most recently-published element. A PassthroughSubject drops values if there are no subscribers, or its current demand is zero. (參考 Apple 文件)

在初始化部分,我們利用 URLSession 指令從遠端讀取並下載物件:

class RemoteImageLoader: ObservableObject {
    
    var didChange = PassthroughSubject<Data, Never>()
    var data = Data() {
        didSet {
            didChange.send(data)
        }
    }
    
    init(imageUrlString: String) {
        
        guard let url = URL(string: imageUrlString) else { return }
        
        let task = URLSession.shared.dataTask(with: url) { data, response, error in
            guard let data = data else { return }
            DispatchQueue.main.async {
                self.data = data
            }
        }
        task.resume()
    }
}

完成 RemoteImageLoader 之後,我們宣告一個訂閱此 RemoteImageLoader 的 RemoteImageView。可以看到 .onReceive 那行程式碼,當 RemoteImageView 收到 RemoteImageLoader 已經取得圖片 Date 資料的通知時,我們再利用 UIImage 去讀取 data 資料,轉而存進 Image 裡。

struct RemoteImageView: View {
    
    @ObservedObject var imageLoader:RemoteImageLoader
    @State var image: UIImage = UIImage()
    
    init(withImageURL url:String) {
        imageLoader = RemoteImageLoader(imageUrlString: url)
    }
    
    var body: some View {
        Image(uiImage: image)
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width: 200, height: 200)
            .onReceive(imageLoader.didChange) { data in
                self.image = UIImage(data: data) ?? UIImage()
            }
    }
}

完成上面兩項任務,我們就可以開始使用 RemoteImageView 來讀取遠端圖片囉!我們就試著讀取這顆蛋黃寶寶吧!  (https://i.imgur.com/SbqZHV2.jpg)

struct ContentView: View {
    var body: some View {
        RemoteImageView(withImageURL: "https://i.imgur.com/SbqZHV2.jpg")
    }
}

完整 code 下載

SwiftUI: 自行新增字體

除了 iOS 內建的字體之外,我們也可以自己在 Xcode 中加入想要的字體,在這邊我們以 Nunito 字體為例 (字體下載)。

首先,將下載完的字體檔案加入到專案中:可以用 Drag 方式直接拉進 Project 底下,為了檔案看起來更為乾淨,我們先新增一個 Group,取名作為 fonts,並將我們的 Nunito 檔案放進去。這邊要注意兩點,第一:確保有在此 Target 之下(圖中的 Add to targets 有正確勾選),第二,Xcode 只支援副檔名為 .ttf 或是 .otf 的字體。

接者我們到 info.plist 新增 Key: Fonts provided by application,對應的 item 打入我們要使用的字體檔案。注意 Xcode 13 以上直接到 Target 裡面對應的 Info 修改:

最後,我們到程式碼裡面輸入字體名稱即可使用囉!

Text("Hello, world!")
            .font(Font.custom("Nunito-Regular", size: 20))

參考:

SwiftUI: 文字標籤 Text

本篇來介紹在 SwiftUI 裡面所使用的文字標籤,它也是在介面中最為基本的一個零件。在專案一創建時,預設的 code 就會如下,在介面中打印出 “Hello, world!” 字樣:

struct ContentView: View {
    var body: some View {
        Text("Hello, world!")
            .padding()
    }
}

最初預設的字體是 San Francisco。要改變文字標籤的樣式,我們可以使用 .font() 的 modifier,我們可以進一步使用系統所提供的標準樣式,包含 .largeTitle, .title, .title2, .title3, .headline, .subheadline, .body, .callout, .caption, .caption2 以及 .footnote。

Text("Hello, world!")
     .font(.largeTitle)

或者我們可以自行指定固定的 font size:

Text("Hello, world!")
    .font(.system(size: 32.0))

也可以在指定字體粗細與樣式:

Text("Hello, world!")
    .font(.system(size: 32.0, weight: .bold))
Text("Hello, world!")
    .font(.system(size: 32.0, weight: .bold, design: .rounded))
Text("Hello, world!")
            .font(.system(size: 32.0, weight: .light, design: .monospaced))

我們也可以使用預設的樣式再加上粗細或斜體設置:

Text("Hello, world!")
    .font(.title).bold().italic()

以上我們都使直接使用系統字體,我們也可以選擇其他字體,這個網站非常方便得可以讓開發者尋找想要的字體樣式名稱:http://iosfonts.com/

挑選好想要的字體即可直接複製字體名稱到程式碼中,如下:
Text("Hello, world!")
    .font(Font.custom("ChalkboardSE-Bold", size: 20))

另外可以再加上 .foregroundColor 來更改文字顏色:

Text("Hello, world!")
    .font(Font.custom("ChalkboardSE-Bold", size: 20))
    .foregroundColor(.blue)