[iOS 教學] 在 Apple M1 機器上面正確編譯 iOS 專案

蘋果在 2020 年 11 月發表了新的 Macbook 系列,
與新的 Apple M1 晶片 (Apple silicon),效能與省電上推向另外一個新的層次,
在軟體相容上推出了 Rosetta,利用軟硬體的設計,直接讓 arm 架構的 Apple M1 相容 x86_64 程式。

想當然爾,蘋果推出的 xcode 肯定會支援新的 arm 架構,但原有專案甚至套件管理程式並不完整支援。在 iOS 開發上要怎麼調校,才可以正確編譯,甚至效能上比 Intel 晶片更好呢?
請繼續看下去。

原有專案

原有專案是一個有啟用 CocoaPods 的一個 iOS 專案,其中有若干的套件。
(這應該是一個蠻常見的狀態)
沒調整前沒辦法在 Apple M1 晶片中正確編譯,但在 Intel 晶片上卻可以。

硬體

寫之前先解釋一下筆者的硬體:

  • 型號:MacBook Pro (13-inch, M1, 2020)
  • 晶片:Apple M1
  • macOS 版本:Big Sur 11.5
  • xcode 版本:12.5.1 (12E507)
  • Cocoapod 版本:1.11.2
  • Ruby 版本:2.6.3p62 (2019-04-16 revision 67580) [universal.arm64e-darwin20]

錯誤訊息

錯誤訊息可能會是這個:

Could not find module 'FrameworkName' for target 'arm64-apple-ios-simulator'; found: x86_64-apple-ios-simulator, x86_64

導致其中一個函式庫無法正確跑在模擬器上。

操作步驟

  1. 安裝 Ruby-FFI ,在 x64 模式來安裝
$ sudo arch -x86_64 gem install ffi

Ruby-FFI 大致是一個動態連結原生元件庫的一個元件庫。說明如下:

Ruby-FFI is a gem for programmatically loading dynamically-linked native libraries, binding functions within them, and calling those functions from Ruby code. Moreover, a Ruby-FFI extension works without changes on CRuby (MRI), JRuby, Rubinius and TruffleRuby. Discover why you should write your next extension using Ruby-FFI.

  1. 用 x64 模式來執行 CocoaPods 的各種操作,
    例如最常用的: pod install

原本是:

$ pod install

改成這樣:

$ arch -x86_64 pod install

其他操作以此類推。

  1. [極重要]Pods 的專案中的 Build settings 設定
    Build Active Architecture only 改成 NO

這選項設定成 NO 的意思是,不只編譯當前架構的二進位檔,反而編譯「所有」架構的二進位執行檔。
(中文翻譯大概是這樣)
所有架構,我列出一些已知的,例如:
arm64 arm64e armv7 armv7s i386 x86_64

一次全編譯。

另外一個做法,修改你的 Podfile 檔案,
原理就是用程式的方式,將所有依賴元件,全部修改 Build Active Architecture onlyNO

可以直接把這段寫進 Podfile 中最後面

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |configuration|
      target.build_settings(configuration.name)['ONLY_ACTIVE_ARCH'] = 'NO'
    end
  end
end

這樣 arm 架構的 iOS 模擬器就可以吃到正確的架構執行檔並執行了。

參考資料

[iOS] 跟 Sign in with Apple 的愛恨情仇

最近終於研究出如何使用 Sign in with Apple 包含前後端的串接,分享給大家。
(本文同步於 iPlayground 2020 演講)


投影片:

iPlayground 影音版:https://www.youtube.com/watch?v=DwXhZwRDlzk


為什麼要串?

在 Apple 開發者大會 WWDC 2019 發表了 Sign in with Apple 的功能,iOS 13 後皆支援。

主要二個重點:

  • 使用 Apple ID 登入,強化保護您的隱私權
  • 當用戶不願給出自己的 E-mail 時,蘋果可以生成一組
    「虛擬 E-mail」給用戶使用,
    「虛擬 E-mail」收到的資訊,蘋果會代轉給用戶的「真實 E-mail」

官方消息指出,從 2019 年 9 月 12 日開始,新上架的 APP 需設置 Apple ID 登入;

在 2020 年 4 月之後,蘋果直接強制要求,
只要您的 app 有支援第三方登入 (例如:Facebook 登入、Google 登入、Twitter 登入…等)
就「一定要」支援 Sign in with Apple (硬要多一顆按鈕),相當於強迫中獎的概念。

基本上只要有帳號系統的服務網站,幾乎全部得加。

否則會怎麼樣呢? app 送審不會過,新 app 無法上架,新的 app 更新無法推上架提供更新。

串接 Sign in with Apple 勢在必行,因為 App Store 是 Apple 在管,官方擁有至高無上的權力。

Apple Sign In 的原理

Apple Sign In 主要接近於標準的 OAuth 流程,我們主要分成 網站 跟 App 二個部分來說:

App 登入

  1. 在 App 中,使用者按下 Sign in with Apple 按鈕,呼叫相關 AuthenticationServices framework 的相關函式,讓 iOS 系統跳出登入確認
  2. iOS 系統跳出 AppleId 登入確認,利用 TouchId 指紋辨識或 FaceId 臉部辨識(可選擇是否隱藏其 E-mail)
  3. 登入授權成功,系統呼叫 didCompleteWithAuthorization,我們處理該 Callback 將相關參數,用 API 傳回自己的網站
    (如果不做伺服器驗證的話,到這邊就結束了,但不建議)
  4. 在我們的網站 API 收到相關參數後,拿著參數向 Apple 驗證,並取得該使用者資訊
  5. 拿到使用者資訊之後,進行驗證登入,如果沒有帳號者會自動創立帳號

網站登入

  1. 在我們的網站,使用者按下 Sign in with Apple 按鈕,呼叫指定網址,其中帶入 Redirect URI 轉址回跳我們網站的網址,瀏覽 Apple 網站進行登入與授權
  2. 使用者在 Apple 網站進行登入,輸入 AppleId 的帳號密碼,
    並授權開發商授權允許(可選擇是否隱藏其 E-mail )
  3. 登入授權成功,轉跳回我們的網站,並附上一些的參數
  4. 我們的後端,拿著 Apple 傳回的參數向 Apple 驗證,並取得該使用者資訊
  5. 拿到使用者資訊之後,進行驗證登入,如果沒有帳號者會自動創立帳號

流程有一點接近但不太一樣,怎麼向 Apple 驗證這個之後會說。

虛擬 E-mail

剛剛有提到,當用戶不願給出自己的 E-mail 時,蘋果可以生成一組
虛擬 E-mail 給用戶使用,
這個虛擬 E-mail 是 亂碼@privaterelay.appleid.com
它是每個 App 專屬的,每一組 E-mail 都不一樣,
蘋果會驗證寄件人與寄件來源(不是什麼信件蘋果都會轉)

刪掉重裝又會產生一組新的虛擬 E-mail,讓使用者可以在他的設定頁,充分控制自己的個資露出於否。

但要實現這個功能,需要設定寄件人,讓指定的寄件人可以寄給使用者。

如果您是用自訂網域的話,則要綁定您的網域,蘋果他會用 SPF (Sender Policy Framework) 來驗證寄件來源。換句話說,如果要讓這個 虛擬 E-mail 能夠正常收到您寄的信,看你網域常用發信服務是誰,在您的網域 DNS 設定對應的 SPF DNS Record。

蘋果開發者網站設定

在 Apple 的開發者網站 ( https://developer.apple.com/ ) 當然要做很多設定

一樣分成網站 跟 App 二個部分來說:

App 部分

在 Certificates, Identifiers & Profiles -> Identifiers -> App IDs

按旁邊 + 號,選擇 App ID,選擇 App (不是 App Clip)
來創建您的 App ID(如果已經建立的話就修改它)

  • Description
    這裏填入您的 App 名字,
  • Bundle ID
    選擇 Explicit ,並填入 Xcode 裡的 Bundle identifier

(這應該是一般 IOS 開發者會做的事情)

今天要勾選 Sign In with Apple,並按下 Edit

  • Sign In with Apple: App ID Configuration
    留預設值 Enable as a primary App ID 即可
  • Server to Server Notification Endpoint
    這裡要填入一個網址,接收處理從 Apple 傳來的通知,後面會提到

網站部分

在 Certificates, Identifiers & Profiles -> Identifiers -> Service IDs

按旁邊 + 號,選擇 Service ID 來創建您的 Service ID
這裡有二個選項要填:

  • Description
    這裏填入您網站的名字
    (注意:這個值會顯示在前台網站給使用者看到)
  • Identifier
    你可以隨意取一個,作為辨識用
  • 勾選 Sign in with Apple 並按旁邊的 Configure

在 Web Authentication Configuration 頁面

  • Primary App ID
    選擇你主要 App 的 App ID
  • Register Website URLs

    • Domains and Subdomains
      這裏填入您網站的網域
      (注意:你的網站必須要有 https,不可用 localhost 或者 IP ,否則這裡不會過)

    • Return URLs
      這裡填入回跳用的 Redirect URI
      (一樣的規則,你的網站必須要有 https,不可用 localhost 或者 IP ,否則這裡不會過)

    如果是測試用的話,可以使用測試用的 Redirect URI:

    https://example-app.com/redirect

    (他是一個合法的網站,不是亂寫唬爛的)
    轉導到 example-app.com 之後藉由您手動複製貼上的方式把回傳值帶到你本機的測試程式中

Sign Key 驗證金鑰

我們需要建立一個 Sign Key 在等一下跟蘋果 API 做驗證使用,這部分因為網站跟 App 驗證流程後半段是一樣的,不管支援哪一個部分都要做。

在 Certificates, Identifiers & Profiles -> Keys

按旁邊 + 號,建立一個 Key

  • Key name
    可以自行取名
  • 勾選 Sign in with Apple 並按旁邊的 Configure
    • Primary App ID
      選擇主要 App 使用的 App ID
    • Grouped App IDs
      取名會把網站跟 App 群組綁在一起

按下 Continue 之後會讓你下載一個私鑰 p8 檔案,
注意這只能被下載一次,請好好保存。
如果不見的話就只能再重新產生一個。

設定寄件人、寄件網域

在 Certificates, Identifiers & Profiles -> More -> Sign in with Apple for Email Communication

按旁邊 + 號,輸入您的自訂網域與寄件人。

寄件人可以設定 Gmail 會直接通過。

如果自訂網域,需設定 SPF (DNS Sender Policy Freamwork)

設定 SPF

如果您是用自訂網域的話,則要綁定您的網域,蘋果他會用 SPF (Sender Policy Framework) 來驗證寄件來源。換句話說,如果要讓這個 虛擬 E-mail 能夠正常收到您寄的信,看你網域常用發信服務是誰,在您的網域 DNS 設定對應的 SPF DNS Record。

這遍設定各家有點不一樣

需要新增一個 TXT Record
值為

"v=spf1 include:_spf.google.com include:sendgrid.net include:amazonses.com ~all"

這個是範例值,裡面包含三個郵件服務 Email Service Provider (ESP)

  • Google (Gsuite) (Gmail)
  • SendGrid
  • Amazon SES

看您的郵件服務是哪一家,找到那家業者,照這個方式設定您允許的郵件服務存取您的網域

做一個簡單整理

  • CLIENT ID
    它可以是 App ID (也就是 Bundle ID) 也可以是 Service ID。

    • 如果要 App 端做登入,它就會是 App ID (也就是 Bundle ID)。
    • 如果要 網站端做登入,它就會是 Service ID。
  • REDIRECT URI
    OAuth 之後要轉跳的網址

    • App 的部分在 App ID 裡面做設定
      Certificates, Identifiers & Profiles -> Identifiers -> App IDs -> 您的 App ID -> Configure -> Return URLs
    • 網站的部分在 Service ID 裡面做設定
      Certificates, Identifiers & Profiles -> Identifiers -> Service IDs -> 您的 Service ID -> Configure -> Return URLs
  • TEAM ID
    你的開發者帳號 Team ID,這可以在你的右上角看到
    進去 Certificates, Identifiers & Profiles -> Identifiers -> App IDs -> 您的 App -> App ID Prefix 可以看見

  • KEY ID
    您建立驗證的 Sign Key 的 Key ID
    在 Certificates, Identifiers & Profiles -> Keys -> 您的 Apple Sign Key -> View Key Details -> Key ID 可以看到

  • SIGN KEY
    剛剛產生的 Sign Key 私鑰( p8 檔案 )

App 端實作

這裏你可以使用 Apple 所使用的 Sign In with Apple 按鈕,
Sign In with Apple 的按鈕 這裡

這裡有按鈕樣式規範與素材:
https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple/overview/buttons/

我使用的是直接呼叫的方式

import Foundation
import AuthenticationServices

class AppleSignInManager: NSObject {
    var currentView: UIView?

    func signIn(currentView: UIView) {
        guard #available(iOS 13.0, *) else { return }
        self.currentView = currentView
        let provider = ASAuthorizationAppleIDProvider()
        let request = provider.createRequest()
        request.requestedScopes = [.email, .fullName]
        request.nonce = "[NONCE]"
        request.state = "[STATE]"
        let controller = ASAuthorizationController(authorizationRequests: [request])
        controller.delegate = self
        controller.presentationContextProvider = self
        controller.performRequests()
    }
}

這邊我寫了一個 AppleSignInManager 來控制整個流程,做一個 signIn() 的 method 呼叫 Sign in with Apple。

要設定

  • state:一個您設定的,辨識用的字串
  • nonce:一個您產生的,辨識用的亂碼

來避免 CRSF 跨網域攻擊

extension AppleSignInManager: ASAuthorizationControllerDelegate {
    @available(iOS 13.0, *)
    func authorizationController(controller: ASAuthorizationController, 
               didCompleteWithError error: Error) {
        // Handle error
        print(error)
    }

    @available(iOS 13.0, *)
    func authorizationController(controller: ASAuthorizationController, 
               didCompleteWithAuthorization authorization: ASAuthorization) {
        guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential else { return }
        // Post identityToken & authorizationCode to your server
        print(String(decoding: credential.identityToken ?? Data.init(), as: UTF8.self))
        print(String(decoding: credential.authorizationCode ?? Data.init(), as: UTF8.self))
    }
}

這邊要實作一個 ASAuthorizationControllerDelegate,處理登入成功、登入失敗之後的動作,
登入成功要把 identityTokenauthorizationCode 等資訊 回傳給您的伺服器,繼續做驗證。

extension AppleSignInManager: ASAuthorizationControllerPresentationContextProviding {
    @available(iOS 13.0, *)
    func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
        return self.currentView?.window ?? UIApplication.shared.keyWindow!
    }
}

這邊要實作一個 ASAuthorizationControllerPresentationContextProviding,回傳目前當下 ViewController 的 window 給它,它會在 Sign in with Apple 呼叫後,把系統的畫面放在它上面。

網站前端實作

文件在這:
https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/configuring_your_webpage_for_sign_in_with_apple

你可以使用蘋果他預設給你的按鈕,

  • 字樣有 Sign in with Apple 或者 Continue with Apple 二種(包含多國語言)可以選
  • 樣式有 黑色白色 二種樣式,還有要邊框 (border) 與否可以選

或者你可以照個他的規範,自訂一個按鈕來用

按鈕樣式的規範與素材在這:
https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple/overview/buttons/

OAuth 跳轉模式有二種可以選:

  • 直接頁面轉跳
  • 開新視窗(彈窗)登入後關閉(一般 popup window 的做法)

前者較簡單,後者要自行處理流程

文件上提到的寫法就有二種,不過意思是一樣的:

<html>
    <head>
    </head>
    <body>
        <script type="text/javascript" src="https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js"></script>
        <div id="appleid-signin" data-color="black" data-border="true" data-type="sign in"></div>
        <script type="text/javascript">
            AppleID.auth.init({
                clientId : '[CLIENT_ID]',
                scope : '[SCOPES]',
                redirectURI : '[REDIRECT_URI]',
                state : '[STATE]',
                nonce : '[NONCE]',
                usePopup : true //or false defaults to false
            });
        </script>
    </body>
</html>
  • clientId:這遍填入你的 Services ID
  • scope:我都填固定值 name email (注意:中間有空格)
  • redirectURI:填入登入成功後要轉跳的網址,必須是 https 不能為 localhost 或者 IP
  • state:一個您設定的,辨識用的字串
  • nonce:一個您產生的,辨識用的亂碼
  • usePopup:是否用開新視窗(彈窗)登入,預設值是否

我使用的是自訂按鈕搭配 JavaScript 直接呼叫的方式:

async function doAppleSignIn() {
    try {
        const data = await AppleID.auth.signIn();
        console.log(data);
    } catch (error) {
        //handle error.
    }
}

上面的 AppleID.auth.signIn() 它會啟動整個 OAuth 流程。
如果是使用彈窗模式的話,會收到回傳的內容,如果是一般模式的話,會用頁面轉跳的。

成功登入時,如果是用轉跳的方式,會轉跳你設定的 REDIRECT_URI 並且傳入幾個參數供使用。

  • code
    單次使用的 Authentication code (效期只有 5 分鐘)

  • id_token
    identityToken 是一個包含用戶資訊的 JSON web token (JWT)

  • state
    你定義的字串

  • user
    一些 firstName, lastName, email 等使用者資料
    (不過蘋果這麼注重用戶隱私,也拿不到什麼資料)

如果是開新視窗登入,需要另外處理事件

//Listen for authorization success
document.addEventListener('AppleIDSignInOnSuccess', (data) => {
     //handle successful response
});
//Listen for authorization failures
document.addEventListener('AppleIDSignInOnFailure', (error) => {
     //handle error.
});

這裡你可以打你自己設計的 API,
identityTokenauthorizationCode 傳回給你自己的伺服器繼續做驗證

網站後端實作

JWT (JSON Web Token)

Apple Sign In 的資料交換主要使用 JWT (JSON Web Token) 的格式
(更明確點說,他是一個 JSON Web Signatures (JWS) 的格式)

在這之前,我們先釐清一下一些相關名詞:

  • RFC 7519 – JSON Web Token(JWT)
    定義了 header 內容與 claim 內容,以及 token 的相關規範

    • RFC 7515 – JSON Web Signature(JWS)
      定義如何做帶有簽章的 token
    • RFC 7516 – JSON Web Encryption(JWE)
      定義內容加密的 token
  • RFC 7517 – JSON Web Key(JWK)
    定義金鑰的格式
  • RFC 7518 – JSON Web Algorithms(JWA)
    定義加解密的演算法

所以

  • JWS 與 JWE 都是屬於 JWT 的一種。
  • 如果沒特別說明,則 JWT 皆是指 JWS。

我們再來說明一下什麼是 JWT

JWT 的全名是 JSON Web Token,是一種基於 JSON 的開放標準(RFC 7519),它定義了一種簡潔(compact)且自包含(self-contained)的方式,用於在雙方之間安全地將訊息作為 JSON 物件傳輸。而這個訊息是經過數位簽章(Digital Signature),因此可以被驗證及信任。可以使用 密碼(經過 HMAC 演算法) 或用一對 公鑰/私鑰(經過 RSA 或 ECDSA 演算法) 來對 JWT 進行簽章。

它是用 .(點)來分隔,主要有三個部分 base64UrlDecode 的內容:

  • Header
  • Payload
  • Signature/Encryption data

前二者用 Base64UrlDecode 解碼後,各會是一個 JSON 資料,
而 signature 故名思義就是一個用演算法算出來的 Hash

展開說明如下:

  • Header

    • alg
      必要欄位,對此 JWT 進行簽章、加解密的主要演算法 (JWA)。
      (這個名字叫做 JWA (JSON Web Algorithms) )
      這裡列出幾個常見的:

      • HS256 (HMAC-SHA256)
      • RS256 (RSA-SHA256)
      • ES256 (ECDSA-SHA256)

      第一項只有單向由同一把金鑰做雜湊 (Hash)。
      而後二者為非對稱式加解密,由私鑰進行簽名,由公鑰進行驗證。

    • typ
      JWT 本身的媒體類型,在 Sign In with Apple 這裡,
      我們使用預設值 JWT

    • kid
      這邊是 Apple 定義的 Sign Key 的 Key ID。

  • Payload
    • iss
      Issuer 的簡稱,表示發行者。
    • aud
      Audience 的簡稱,表示接收者。
    • iat
      Issued at (time) 的簡稱,即該 JWT 發行的時間,用 Unix timestamp 表示(POSIX 定義的自紀元以來的秒數)。
    • exp
      Expiration (time) 的簡稱,即該 JWT 過期的時間,格式一樣為 Unix timestamp。
    • sub
      Subject 的簡稱,用字串(case-sensitive) 或 URI 表示這個 JWT 所夾帶的唯一識別訊息。

怎麼驗證?

驗證方式有二種方式:

  • identityToken 用 JWT 的格式定義來驗證是否為 Apple 所簽發的
  • authorizationCode 用 OAuth 的機制向 Apple 伺服器交換並要求 Access Token

前後者不衝突,也可以二者都做,看你的需求。

驗證 IdentityToken

其實從 app 端拿到的 identityToken 它本身也是一個 JWT 格式

payload 用 base64UrlDecode 解開後可以得到類似以下的資料

Header 部分
{
  "kid": "86D88Kf",
  "alg": "RS256"
}

你可以看到他是用 RS256 (RSA-SHA256) 做加密簽章的,
由蘋果伺服器所擁有的私鑰進行簽名,由蘋果提供的 API 取得公鑰進行驗證。
它的 Key ID 為 86D88Kf (辨識是蘋果哪一把 Key 簽的,這等下會說)

Payload 部分
{
  "iss": "https://appleid.apple.com",
  "aud": "com.your.app.id",
  "exp": 1596621649,
  "iat": 1596621049,
  "sub": "001451.3dc436155xxxxxxxxxxxxxxxxxxxx59f.0447",
  "c_hash": "iUqI9Vyxxxxxxxxxg-CyoA",
  "email": "[email protected]",
  "email_verified": "true",
  "is_private_email": "true",
  "auth_time": 1596621049,
  "nonce_supported": true
}

取重點說明:

  • iss:發行者(issuer)
    https://appleid.apple.com,蘋果的伺服器
  • aud:接收者(audience)
    com.your.app.id (您的 App Id)
  • sub:Subject 主題的值,是使用者辨識唯一識別碼
    (可作為 使用者 ID 當做判斷依據)
  • email:使用者的 Email,範例值為虛擬 E-mail,
    如果用戶選擇不隱藏 E-mail,這裡就會顯示真實用戶的 E-mail
  • exp:過期時間
Signature 部份

從 Header 可以知道,
蘋果是用 86D88Kf 這把私鑰用 RS256 (RSA-SHA256) 簽的,等下取得該 86D88Kf 的公鑰就可以驗證它。

取得蘋果的公鑰

這裡蘋果有提供 API 來取得公鑰,

蘋果 API 文件在此:
https://developer.apple.com/documentation/sign_in_with_apple/fetch_apple_s_public_key_for_verifying_token_signature

用 GET 來打以下網址取得:

https://appleid.apple.com/auth/keys

它是一個標準 JSON Web Key Set (JWKS) 格式,
會得到類似這樣的資料

{
  "keys": [
    {
      "kty": "RSA",
      "kid": "86D88Kf",
      "use": "sig",
      "alg": "RS256",
      "n": "iGaLqP..................zHLwQ",
      "e": "AQAB"
    }
  ]
}

礙於篇幅縮減了一下資料,重點說明:

  • kid:金鑰 ID,你可以找到剛剛範例的 86D88Kf 這個金鑰
  • alg:使用的演算法,範例值是 RS256
  • use:用途描述,白話文就是拿來幹嘛用的
    sig 意思就是拿來簽章用的
  • n:RSA 模數 (e),公鑰內容的一部分
  • e:RSA 指數 (n),公鑰內容的一部分

簡單來說,我們要:

  1. kid 找到對應的金鑰,
  2. ne 這二個值還原回 PEM 格式,
  3. 對 identityToken 做 Signature 驗證,檢核是否為蘋果伺服器發的
  4. 取用裡面的資料

以下使用 PHP 搭配 Lcobucci/JWTFirebase\JWT 套件來實作

require_once '../vendor/autoload.php';
function getUserDataFromIdentityToken($idToken)
{
    $token = (new Lcobucci\JWT\Parser())->parse((string)$idToken);

    $applePublicKeysRaw = curlGetAppleAuthKeys();
    $applePublicKeys = JWKParseKeySet($applePublicKeysRaw);

    $applePublicKey = $applePublicKeys[$token->getHeader('kid')];

    $signer = new Lcobucci\JWT\Signer\Rsa\Sha256();
    $keychain = new Lcobucci\JWT\Signer\Keychain();   
    if (!$token->verify($signer, $keychain->getPublicKey($applePublicKey))) {
        throw new RuntimeException("Key validation failed.");
    }

    if ('https://appleid.apple.com' !== $token->getClaim('iss')) {
        throw new RuntimeException("Source incorrect.");
    }
    $userData = array();
    $userData['email'] = $token->getClaim('email');
    $userData['id'] = $token->getClaim('sub');
    return $userData;
}

function curlGetAppleAuthKeys()
{
    $ch = curl_init('https://appleid.apple.com/auth/keys');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    $result = curl_exec($ch);
    curl_close($ch);

    return json_decode($result, true);
}

function JWKParseKeySet($keySets)
{
    $parsed = \Firebase\JWT\JWK::parseKeySet($keySets);
    $pemKeySets = array();
    foreach ($parsed as $keyId => $sslKey) {
        $pemKeySets[$keyId] = openssl_pkey_get_details($sslKey)['key'];
    }
    return $pemKeySets;
}

function JWKVerify($idToken, $publicKeyPem)
{
    $signer = new Lcobucci\JWT\Signer\Rsa\Sha256();
    $keychain = new Lcobucci\JWT\Signer\Keychain();
    $token = (new Lcobucci\JWT\Parser())->parse((string)$idToken);
    return $token->verify($signer, $keychain->getPublicKey($publicKeyPem));
}

composer.json

{
  "require": {
    "firebase/php-jwt": "5.2.0",
    "lcobucci/jwt": "3.3.2"
  }
}

使用 AuthorizationCode 交換 AccessToken

這部分也就是最困難的部分

先貼蘋果 API 文件:
https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens

需要 POST 以下網址:

https://appleid.apple.com/auth/token

它定義了幾個參數

  • client_id
    如果是 App 登入就是 App ID (Bundle ID),
    如果是網站登入就是 Services ID。
  • client_secret
    一個 JWT 格式的要求文件,並利用前面申請的 Sign Key 來簽章
  • code
    填入 App 或 網站 收到的 Authorization Code
    (注意:Authorization Code 的效期非常短,文件上說最長只有 5 分鐘效期,
    但實際設定可能只有 30 秒至 1 分鐘左右)
  • grant_type
    這裡我們填 authorization_code 來交換 AccessToken

所以我們要:

  1. 自己組一個 JWT,並利用前面申請的 Sign Key 來簽章(指定使用 ES256 演算法),作為 client_secret,並帶入收到的 Authorization Code 作為參數,
  2. 打蘋果的 API

蘋果有給一個 JWT 的組成範例:

{
    "alg": "ES256",
    "kid": "ABC123DEFG"
}
  • alg:指定使用 ES256 (ECDSA-SHA256)
  • kid:你的 Sign Key 的 Key ID
{
    "iss": "DEF123GHIJ",
    "iat": 1437179036,
    "exp": 1493298100,
    "aud": "https://appleid.apple.com",
    "sub": "com.mytest.app"
}
  • iss:發行者(issuer)
    填入你的 開發者帳號的 TEAM ID
  • aud:接收者(audience)
    填入固定值 https://appleid.apple.com
  • iat:填入現在時間 (Unix timestamp)
  • exp:填入過期時間 (Unix timestamp),不能超過六個月
  • sub:Subject 主題的部分填入上述的 client_id
    如果是 App 登入就是 App ID (Bundle ID),
    如果是網站登入就是 Services ID。

最後簽章,組成一個 JWT 當作 client_secret 來打 API

成功的話會有類似以下的回傳值

{
  "access_token": "a6cab...........Y1A",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "r9d77fa9..........gfYA",
  "id_token": "eyJraW.................jMA"
}

其中一個重點, id_token 就是 identityToken,是我們要的資料,
解開後有:

  • sub :Subject 主題的值為 使用者 ID
  • email:值為使用者的 Email

這些前面有提到了,就不在重述。

Troubleshooting

雖然文件定義了很多錯誤訊息
https://developer.apple.com/documentation/sign_in_with_apple/errorresponse

大致會出現的只有幾種:

  • invalid_request 通常是連參數都弄錯
  • invalid_client 可能是 client_id 弄錯,
    client_secret (JWT 格式) 裡的 subclient_id 參數不吻合,或者根本沒這個 client_id
  • invalid_grant 這個是最常見(也是最難排查的)的錯誤,
    • 可能是 client_secret JWT 格式的加解密算錯 ⚠️
    • 可能是 Authorization Code 過期(沒即時打 API 做交換)
      (Authorization Code 效期超短,通常只有 1-2 分鐘,不超過 5 分鐘) ⚠️
    • 可能是 Authorization Code 已經被交換掉,變成無效 Code ⚠️
      (API 帶相同參數重複打就會出現)

Server to Server Notification

這個功能在新的 WWDC 2020 影片中推出,
截稿至今,蘋果還沒有寫 API 文件,只有釋出影片解釋 😓。

詳述在這裡

設定上去之後,蘋果會在用戶有帳號變更的時候通知你。

蘋果會用 POST 傳一個 json 格式給你,格式大致如下:

{
  "payload": "eyJraWQxxxxxxxxxxxxxxiUlMyNTYifQ.eyJpcxxxxxxxxxxxxxc0fSJ9.IUFWxPxxxxxxxxxxxxxxxxxbL3olA"
}

只有一個值叫做 payload,裡面也是一個 JWT 格式
你把 JWT 其中的 payload 用 base64UrlDecode 解開,會得到以下格式

{
  "iss": "https://appleid.apple.com/",
  "aud": "<Bundle Identifier>",
  "iat": 1508184845,
  "jti": "<unique events stream id>",
  "events": [
    {
      "type": "email-disabled",
      "sub": "<user_id>",
      "email": "<[email protected]>",
      "is_private_email": true,
      "event_time": 1508184845
    }
  ]
}

重點說明如下:

  • iss:發行者(issuer)
    https://appleid.apple.com,蘋果的伺服器
  • aud:接收者(audience)
    com.your.app.id (您的 App Id)
  • iat
    Issued at (time) 的簡稱,即該 JWT 發行的時間,用 Unix timestamp 表示。
  • jti:事件唯一碼
  • events:事件
    • type:目前狀態
    • sub:用戶唯一碼
    • email:用戶電子信箱(有可能是虛擬 Email)
    • is_private_email:是否為虛擬 Email
    • event_time:事件時間

其中 type 狀態有幾種:

  • email-enabled
  • email-disabled
  • consent-revoked
  • account-delete

祝串接順利。 🙂


參考資料

[iOS教學] 使用 PromiseKit 來管理你的 callback!

請參考 PromiseKit 的文件:

Promise 基本概念

在非同步執行的流程處理上,傳統作法一直是個麻煩點,
而 Promise 透過一些函式可以很直覺的管理非同步的流程。

傳統的做法

以 iOS 而言,你可能要透過 NSOperationQueue 或者
GCD (Grand Central Dispatch) 這些方法來做非同步的流程。

如果要接續觸發( A 事情做完做 B )的情境,A 事情的 callback 做完之後緊接著 B 事情,你可能會得到一個很深的縮排。
如果是互相等待完成( A 跟 B 事情 )的情境,你可能要透過一些 boolean 來把狀態記住,然後 A 事情跟 B 事情的 callback 同時要檢查對方是否做完,才接續另外一個 callback 。 以上並不是說這樣寫不能用,只是你會有更好的解法,讓程式碼變得更乾淨好閱讀。

Promise 的做法

主要關鍵字有 firstly then catch 還有 always
要字面上來看就是 「首先」、「然後」最後是有錯誤時用 catch 抓取錯誤。

這個範例還用了一個很實用的 when() 來綜合二件非同步的事情,當二件事情都結束時才會回傳到下一個 Callback。

這是一個綜合各種基本關鍵字的範例

firstly {
    // Show Loading status bar
    return when(myAsnycTaskA(), myAsnycTaskB())
}.then { (resultA:MyObjectA, resultB:MyObjectB) -> Void in
    // Show results
}.always {
    // Hide Loading status bar
}.catch { error in
    print(error)
}

最簡單的範例

這是一個最基本的範例
基本句型有 thencatch 就可以了

myAsnycTaskA().then { 
    (resultA:MyObjectA) -> Void in
    // Show results

}.catch { error in
    print(error)
}

宣告

至於你想要宣告一個事情也不難

func myAsnycTaskA()-> Promise<MyObjectA> {
    return Promise { fulfill, reject in
        // Done
        fulfill(result)

        // Fail
        reject(error)
    }
}

回傳一個 Promise,裡面有 fulfill()reject() 二個 method。
當資料回來的時候呼叫 fulfill()
當出現錯誤的時候呼叫 reject() 並帶入一個 Error 物件。

複雜一點的範例

這時回過頭來看第一個範例,我又把它改複雜了一點,是不是比較不難了呢?

firstly {
    // Show Loading status bar
    return when(myAsnycTaskA(), myAsnycTaskB())
}.then { (resultA:MyObjectA, resultB:MyObjectB) -> Promise<MyObjectC> in
    // So some processing
    return myProcessingTaskC()
}.then { (resultC:MyObjectC) -> Void in
    // Show results
}.always {
    // Hide Loading status bar
}.catch { error in
    print(error)
}

首先,同時做 myAsnycTaskA()myAsnycTaskB()
等二者結果回傳了之後,做 myProcessingTaskC() 最後回傳結果。

在 callback 的處理上是不是變得比較開心愉快了一點呢?
以上就是 Promise 的快速介紹。

有興趣的話,可以查看官方文件裡面有更進階的寫法。
這個概念不只在 iOS (Swift) 可以用,在 JavaScript (ES6) 也有類似的語法,有機會再專文介紹。

[iOS] Apple iOS Developer Program 開發者帳號 申請實錄

Screen Shot 2013-07-08 at 1.24.24 AM  

話說 iOS開發者帳號已經想申請想很久了

最近心一橫,拿出信用卡
就給它刷下去了

 

但是…申請過程沒有那麼順利(我申請了二次)

所以就把一些東西記錄下來,給大家參考


敗家 阿不是,是學習大門的網址都幫你準備好了:

https://developer.apple.com/programs/ios/

看到 iOS Developer Program

的藍色Enroll Now的按鈕,就用力的給他點下去吧!

Screen Shot 2013-07-08 at 1.26.26 AM  

這裡有大概跟你說整個申請流程
簡單來說選擇1. 申請身份 2. 填資料 3. 線上刷卡及人工審核
直接按Continue繼續

 

Screen Shot 2013-07-08 at 1.29.05 AM  

 

 

申請新的Apple ID

 

這裡說要登入Apple ID,或是新創一個Apple ID帳號去綁開發者帳號

我是選Create Apple ID去新創一個Apple ID

到這裡都還算簡單

Screen Shot 2013-07-08 at 1.32.16 AM  

 

然後就是填Apple ID的資料…資料都要真實的
因為到時候Apple那邊會查

Screen Shot 2013-07-08 at 1.33.40 AM  

這裡請特別注意…Middle Name請不要雞婆的去填寫它,乾脆說請直接忽視Middle Name的欄位

台灣人不需要這個欄位

只要依照指示填寫First Name姓氏,Last Name名字,就好

Screen Shot 2013-07-08 at 1.38.29 AM  

至於地址的部份,我當時是寫英文啦

只是Preferred Language是填Chinese就是了

送出之後呢…就創立一個 Apple ID了


但還沒完,還沒有填到重點的部份

 

 

開發者帳號申請

Screen Shot 2013-07-08 at 1.43.12 AM  

它會問你,你要申請哪種開發者帳號

Individual (個人) 還是Company/Organization (公司)

Company的部分我個人沒申請過…

我是選擇 Individual


Screen Shot 2013-07-08 at 1.48.46 AM  

看到很多黃色區塊了吧?這幾步非常的重要

它說,這個名字一定要是你信用卡申請人的名字

所以像圖上的範例就是

意思就是說….不能用他人的信用卡代刷這筆費用

而且這名字一定要是你的真名

而且還有一個重點,

申請送出了之後(包含錢),資料就無法做修改

所以要特別特別的填清楚

當然,Middle Name的欄位請略過…台灣人不需要這個欄位

至於填中文還是英文,Apple客服說都可以

 

但一定要讓他們對得到資料

Screen Shot 2013-07-08 at 1.57.01 AM  

地址欄位欄位也是,送出後無法修改
我就不贅述了

Screen Shot 2013-07-08 at 2.02.45 AM  

資料確認頁,確定無誤就按下一步
再次提醒你,這裡的Name打上的是Johnny Sung是

你必須要打上你的真名

Screen Shot 2013-07-08 at 2.04.17 AM  

Apple授權協議頁,他很貼心的給你PDF的版本
按 I Agree繼續

Screen Shot 2013-07-08 at 2.06.34 AM  

頁面會幫你轉到Apple Store的商店

加入iOS Developer Program這個項目到你的購物車裡

 

Screen Shot 2013-07-08 at 2.06.56 AM  

這邊有沒有覺得很神奇,突然什麼都變成中文了

我想可能是Preferred Language是填Chinese的關係

按下「結帳」繼續

 

Screen Shot 2013-07-08 at 2.07.50 AM  

直接按下「繼續」

Screen Shot 2013-07-08 at 2.08.09 AM

這邊很神奇,它又要你填一次地址

這次沒得選,只能填中文(它的表單有設計過,你填英文會不夠長)

我想這個欄位應該是沒有使用到

Screen Shot 2013-07-08 at 2.09.18 AM  

這裡就很重要了,要你填入信用卡的卡號 安全碼等等的資料

請容許我在重述一次

這個名字一定要跟你剛剛填的申請人名字一樣

所以….不能用他人的信用卡代刷這筆費用

而且這名字一定要是你的真名

申請送出了之後(包含錢),資料就無法做修改

按下「繼續」之後

按下同意授權條款就結束了


你的信箱會收到一封類似這樣的信

Screen Shot 2013-07-08 at 2.23.04 AM  

能幹嘛呢?只有等…還是等…

 

Screen Shot 2013-07-08 at 2.28.27 AM  

大概在24小時之內,會收到一封啓動序號的信

Screen Shot 2013-07-08 at 2.30.57 AM  

順著啓動序號的連結會來到這個網頁

按下Activate鍵就會啓動…


你認為這麼簡單嗎?別高興太早

那個按鈕,就算你是用合法的序號也是會被擋下來的

因為他有人工驗證的部份,人工驗證沒過
一切都白搭

 

如果你有看我文章(?),很細心的注意每個步驟和表單

你就會收到這個成功信件

Screen Shot 2013-07-08 at 2.39.02 AM  

這封信的大意是說,我們已經對過你提供的資料,並移除了擋板

你只要在重新到啓動碼頁面  點一下就會啓用開發者帳號了

 

=========================================

 

 

 

退換貨處理

如果你像我一樣,資料沒有打對的話
要怎麼辦?

 

首先,你會收到類似這樣的信件

Screen Shot 2013-07-08 at 2.37.54 AM  

這段文字很讓人飆髒話,所以貼出來

In order for us to verify your identity, you need to upload a notarized or solicitor certified copy of your government issued photo identification.

就是說,你提供的資料不符,他要求你要上傳一個經過公證單位或律師(solicitor) 公證過的有照片的文件

 

這時候有FAQs 跟 Contact us可以選

Screen Shot 2013-07-08 at 3.11.30 AM  

 

我當下直接選了Call us

https://developer.apple.com/contact/phone.php

Screen Shot 2013-07-08 at 3.18.00 AM   

直接找台灣的電話打就行了,還好是0800免付費電話

跟客服講你現在的情況,還有跟他說你的Apple ID

他就會把原因告訴你

 

打電話了之後,你現在只有二個選項:

  1. 乖乖的去把你的證件拿去公證單位公證然後上傳我是沒有去公證過啦,據網路上查詢的結果 

    根據外交部領事事務局的網頁
    http://www.boca.gov.tw/content?mp=1&CuItem=4990
    倘國、內外要證機關無特別規定要求護照影本須先經國內地方法院公證處或民間公證人證明,申請人可直接向本局或本部中部、南部、東部、雲嘉南四辦事處申辦出具中華民國護照影本與正本相符之證明。 

    蓋個章,規費 $400

    若真的要公證,需要到 台北地院公證處
    http://lawtw.com/article.php?template=article_content&area=free_browse&parent_path=,1,655,7,&job_id=82850&article_category_id=1965&article_id=36859
    中文 $500元,英文 $750元

    根據Apple客服的說法,雙證件也不行
    送出的資料也不能讓使用者或是客服修改資料後重新驗證
    比郵局開戶還難,這點真的會讓人家氣炸

  2. 把這筆申請(包含錢),通通退掉,再重來一次

我是選擇這個選項,他們會照著你的要求做,寄送紙本發票和
折讓單(SAS FORM)到你府上

 

 

你要填一些資料然後簽字寄回去給Apple

Screen Shot 2013-07-08 at 9.27.47 AM  

 


過了幾天之後就會收到這個

LastScan2-2  

統一發票

LastScan-2  

所謂的折讓單(SAS FORM)….上面有個又愛又恨的蘋果

 

這裡我就不重述了,簽一簽,寄回去

然後又是等….


你可以當下拿出信用卡,再刷一次

再走一次這樣的流程

有這麻煩到家的流程,所以一再的提醒你

別打錯字

 

 

折騰了老半天,最後還是申請出來了

大概花了4天吧

這種麻煩事,要走過才會知道有多麻煩
貼出來讓大家更瞭解這機車的流程

有很多地方要注意的點

 

之後我來筆記帳號開通之後能做的事情

謝謝收看。

[iOS筆記] 關於Protocol和物件導向 (Java下的介面(Interface) )

有關於iOS底下的Protocol

 

請參照至其版大的文章

Protocol in Objective-C

http://blog.eddie.com.tw/2010/12/11/protocol-in-objective-c/

這版大實在寫的思路很清晰,推薦一下摟

(看來我還是要多多拜讀別人的文章才是)

————————————————————————————————

為了響應不全文引用,以下採用節錄的,完整內容請移至版大的文章

 

Objective-C是單一繼承的,如果想要做到一個類別同時擁有多種型別的能力,可以透過實作其它型別的interface來達成這個目的。在Java/AS3是用”interface”這個關鍵字,在Objective-C則是用”@protocol”。

(有寫過Java/AS3的要特別注意不要把interface跟protocol搞混了
,在Objective-C的interface等於Java/AS3的class,而protocol則是相當於interface)

Johnny: 關於protocol這點,我也常常搞混…..= =||


如果你要新增一個自定的protocol的話,可以直接在你的專案裡新增一個protocol檔:

image

新增完成之後(它是一個header檔),就可以開始來寫了,

—————————————————————————————————

程式碼如下:

@protocol Drawable

@required
-(void) draw;
-(void) changeColor;

@optional
-(void) whateverMethod;

@end

在上面這段程式碼裡,我放了三個方法,但沒有寫內容。接下來如果我要實作自這個protocol的話,所有定義在@protocol裡的方法都得實作出來。

如果沒特別標明的,預設是@required。如果你要實作這個protocol的話,照英文字面來看,@required的部份是規定要實作的,@optional的話就隨你高興了。要注意的是@required跟@optional這兩個語法的影響範圍,是從它以下所有的method都會被影響,直到另一個directive或是@end為止


————————————————————————————————————————-

實作protocol的方法就是用”<>”標記,裡面放protocol的名稱。並不限定只能實作一個protocol,如果要實作多個protocol的話,則是用逗點分開

因為在protocol的地方已經有定義好了方法,所以在@interface的地方就不用再特別寫一次,只要在@implementation裡補上該實作的方法就行了。

// ————–

// interface
// ————–
#import <Cocoa/Cocoa.h>
#import “Drawable.h”

@interface Book : NSObject <Drawable>
{
int price;
}
@property int price;

@end

// ————–
// implementation
// ————–
#import “Book.h”
@implementation Book

@synthesize price;

// 實作方法draw
-(void) draw
{
NSLog(@”draw me!”);
}

// 實作方法changeColor
-(void) changeColor
{
NSLog(@”change color!”);
}

@end

 

————————————————————————————————————————-

Johnny: 簡單來講,protocol就是Java裡的介面(Interface),上面開出來的方法(method)
且有@required字樣的都要被實作(implementation)

protocol名稱加綴在類別(class)後面
只是為了要讓Complier,確實的去檢查,開出來的方法

是有都通通有實作完畢

實驗發現,某些Class對於protocol要求沒那麼嚴苛

只要實作出它要的方法,而不用加掛這個綴字

而且避免混淆,我現在習慣通通都加上去

(包含放在Class名稱之後的 < > ,和其protocol要的方法一起宣告在header檔)

 

 

我還是覺得還是要學Java一樣,嚴謹一點比較好,較不會出錯

[iOS] 從檔案載入NSMutableArray和NSMutableDictionary 的筆記

在iOS開發上

若是從檔案讀取一些資料,想必會用到

  • NSMutableArray
  • NSMutableDictionary

這二個類別

Mutable是可變動的意思,所以建立之後還可以變動

NSMutableArray的話存取方式採用序號來存取值

而NSMutableDictionary採用自訂的鍵值(key)來存取值(value)

 

二者可以互相混用,NSMutableDictionary裡面有NSMutableArray

或是NSMutableArray裡面包含NSMutableDictionary (就像本次範例)

 

使用initWithContentsOfFile: 方法來載入plist

https://developer.apple.com/library/ios/#documentation/Cocoa/Reference/Foundation/Classes/NSData_Class/Reference/Reference.html#//apple_ref/occ/instm/NSData/initWithContentsOfFile:

NSMutableArray要取得值使用objectAtIndex 方法

https://developer.apple.com/library/ios/documentation/Cocoa/Reference/Foundation/Classes/NSArray_Class/NSArray.html#//apple_ref/occ/instm/NSArray/objectAtIndex:

NSMutableDictionary要取得值使用objectForKey方法

https://developer.apple.com/library/ios/documentation/Cocoa/Reference/Foundation/Classes/NSDictionary_Class/Reference/Reference.html#//apple_ref/occ/instm/NSDictionary/objectForKey:


提外話一下

如果有寫過php的話

他的概念和php的array有一點點類似

http://php.net/manual/en/language.types.array.php

而NSMutableArray類似於

<?php

$array = array("foo", "bar");
var_dump($array);
?>
螢幕輸出:

array(4) { [0]=> string(3) “foo” [1]=> string(3) “bar” }


NSMutableDictionary類似於

<?php
$array = array(
    "foo" => "bar",
    "bar" => "foo"
);
var_dump($array);
?>
螢幕輸出:

array(4) { [“foo”]=> string(3) “bar” [“bar”]=> string(3) “foo” }

 


 

建立一個專案,在專案上按右鍵New File:

Screen Shot 2012-03-25 at 上午10.51.49  

新增一個Property List,名叫article.plist

 

article.plist原始碼如下

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN""http://www.apple.com/DTDs/PropertyList-1.0.dtd">

<plist version="1.0">

    <array>

        <dict>

            <key>title</key>

            <string>title 01</string>

            <key>content</key>

            <string>content 01</string>

        </dict>

        <dict>

            <key>title</key>

            <string>title 02</string>

            <key>content</key>

            <string>content 02</string>

        </dict>

        <dict>

            <key>title</key>

            <string>title 03</string>

            <key>content</key>

            <string>content 03</string>

        </dict>

    </array>

</plist>

 

 做一個Button出來,綁定TestpList_btn_onClick方法,該方法如下

 

– (IBAction)TestpList_btn_onClick:(id)sender {

   

    // 取得專案中內建的pList路徑

    NSString *path = [[NSBundlemainBundle] pathForResource:@"article"ofType:@"plist"];

    NSLog(@"pList Path: %@n", path);

    // pList中的資料從檔案載入

     NSMutableArray *data_pList = [[NSMutableArrayalloc] initWithContentsOfFile:path];

   

    for(NSInteger i=0;i<[data_pList count]; i++)

    {

        NSIndexPath *indexPath=[NSIndexPathindexPathForRow:i inSection:0];

        NSString *title = [[data_pList objectAtIndex:indexPath.row] objectForKey:@"title"];

        NSString *content = [[data_pList objectAtIndex:indexPath.row] objectForKey:@"content"];

        NSLog(@"Index: %i Title: %@ Content: %@n", i, title, content);

    }

}

 

 然後運行程式,按下按鈕之後,在Log這邊得到以下結果:

Screen Shot 2012-03-25 at 上午10.55.55  


順便補充在iOS上的printf裡的格式跟一般的C語言不同 

https://developer.apple.com/library/ios/#documentation/Cocoa/Conceptual/Strings/Articles/formatSpecifiers.html#//apple_ref/doc/uid/TP40004265-SW1

string 要使用 %@

integer 要使用 %i 而不是 %d

 

[iOS] 利用NSURLConnection建立HTTP連線 (GET篇)

最近在研究iOS開發,對於Http連線的好處
在這裡就不再贅述

之前寫過Android版本的,請見:

http://j796160836.pixnet.net/blog/post/28994669

 

雖然我也在學習中,我盡量解釋其方法
如果有誤,還煩請大大們指正

——————————————————————————————————————-

 

開一個新的專案

在Interface Builder版面設計中,拉出一個Button和一個Label

——————————————————————————————————————-

其中 Button Touch Up Instide 中綁定  – (IBAction)request_btn_onclick:(id)sender   方法

LabelReferencing Outlets 綁定  @property (retain, nonatomic) IBOutletUILabel *label;     變數

——————————————————————————————————————-

 

ViewController.h 放入以下宣告

@interface ViewController : UIViewController

{

    NSMutableData *responseData;

}

 

// 開始接收資料,會呼叫此方法

– (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;

// 接收新的資料時,會呼叫此方法

– (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data;

// 下載完畢時,會呼叫此方法

– (void)connectionDidFinishLoading:(NSURLConnection *)connection;

// 連線錯誤時,會呼叫此方法

– (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;

——————————————————————————————————————-

ViewController.m 加入以下程式片段

 

#pragma mark – Connection delegate

– (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response

{

    // 開始下載,重置responseData資料

    NSLog(@”didReceiveResponse”);

    [responseDatasetLength:0];

}

– (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data

{

    // 下載中,附加資料

    [responseDataappendData:data];

}

– (void)connectionDidFinishLoading:(NSURLConnection *)connection

{

    // 下載完成,釋放responseData

    [connection release];

    NSLog (@”succeeded  %d byte received”, [responseDatalength]);

    // 轉譯編碼文字

    NSString *responseString = [[NSStringalloc] initWithData:responseDataencoding:NSUTF8StringEncoding];

   

    [responseDatarelease];

   

    label.text =  responseString;

    [responseString release];

}

– (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error

{

    // 下載錯誤

    label.text = [NSStringstringWithFormat:@”Connection failed: %@”,[error description]];

    NSLog(@”Connection failed! Error – %@ %@”,

          [error localizedDescription],

          [[error userInfo] objectForKey:NSURLErrorFailingURLStringErrorKey]);

}

——————————————————————————————————————-

這段就是按鈕按下去會執行的語法

#pragma mark – Button onClick

 

– (IBAction)request_btn_onclick:(id)sender {

   

    responseData = [[NSMutableDatadata] retain];

    NSString *url = [NSString stringWithFormat:@”http://127.0.0.1/httptest/t.php”];

    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURLURLWithString:url ]];

    [[NSURLConnection alloc] initWithRequest:request delegate:self];

   

}

——————————————————————————————————————-

我簡單解釋一下

剛剛在ViewController之中新增一個 NSMutableDatadata 變數 (Mutable代表可變動的)

然後在 – (IBAction)request_btn_onclick:(id)sender 這裡

指定一個 url 就是我們要瀏覽的位置,GET要上傳的內容就串在後面

再來建立一個 NSURLRequest 和 NSURLConnection 把連線建立起來

回應的部分傳進delegate (委派)之中

 

而delegate (委派)有四個

 

// 開始接收資料,會呼叫此方法

– (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;

當系統開始要接收資料的時候這個方法會被呼叫到
要把這 responseData 給清空,如果UI上有ProgressBar的話要將之歸零

// 接收新的資料時,會呼叫此方法

– (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data;

每次資料在下載的時候,會不停的呼叫這個方法
可以看到這個data傳入的參數,就是每一小塊一小塊的資料
我們只要把它累加起來即可,如果UI上有ProgressBar的話就可以慢慢累加1

// 下載完畢時,會呼叫此方法

– (void)connectionDidFinishLoading:(NSURLConnection *)connection;

最後跑完會呼叫這個方法,就是你需要做處理的部分
例如顯示到畫面上,存入SQLite….等等


// 連線錯誤時,會呼叫此方法

– (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;

最後就是這個,中間若有網路錯誤等因素就會呼叫這個方法
你可以跳一個提示框或是印Log

 

這個範例的url是指到本機的Apache Server的其中的一隻php網頁
你也可以換成yahoo等網站,只是會跑出一堆網頁原始碼而已

可以玩看看 

——————————————————————————————————————-

以下是我不負責任的把語法翻成大家熟悉的Java
對於像我一樣,對Objective-C有閱讀障礙的

可以看一下(當然,底下的程式完全不能跑,就不用複製了….XD)

 

public  class  ViewController  implements  NSURLConnectionDelegate  {

 

   public  static  UILabel  label;

 

   private  NSMutableData  responseData;

 

   public  void  viewDidLoad()  {

 

      responseData  =  new  NSMutableData();

 

      NSString  url  =  “http://127.0.0.1/httptest/t.php”;

 

      NSURLRequest  request  =  new  NSURLRequest(url);

 

      new  NSURLConnection(request,  this);

 

   }

 

 

 

   @Override

 

   public  void  didReceiveResponse(NSURLConnection  connection, NSURLResponse  response)  {

 

      //  開始下載,重置responseData資料

 

      NSLog(“didReceiveResponse”);

 

      responseData.setLength(0);

 

   }

 

 

 

   @Override

 

   public  void  didReceiveData(NSURLConnection  connection,  NSData  data)  {

 

      //  下載中,附加資料

 

      responseData.appendData(data);

 

   }

 

 

 

   @Override

 

   public  void  connectionDidFinishLoading(NSURLConnection  connection)  {

 

      //  下載完成,釋放responseData

 

      connection  =  null;

 

      NSLog(NSString.format(“succeeded    %d  byte  received”responseData.length));

 

      //  轉譯編碼文字

 

      NSString  responseNSString  =  responseData.toNSStringWithEncoding(NSUTF8NSStringEncoding);

 

      responseData  =  null;

 

 

 

      label.text  =  responseNSString;

 

      responseNSString  =  null;

 

   }

 

 

 

   @Override

 

   public  void  didFailWithError(NSURLConnection  connection,  NSError  error)  {

 

      //  下載錯誤

 

      label.text  =  NSString.format(“Connection  failed:  %s”,  error.description());

 

      NSLog(NSString.format(“Connection  failed!  Error  –  %s”,

 

             error.localizedDescription()));

 

   }

 

}

——————————————————————————————————————-

參考資料

http://www.01-labor.com/2011/07/nsurlconnectionhttp.html

http://stm237.iteye.com/blog/1005752

NSURLConnection官方文件

https://developer.apple.com/library/mac/#documentation/Cocoa/Reference/Foundation/Classes/NSURLConnection_Class/Reference/Reference.html

[iOS開發] 讓JB後的iPhone/iPad能夠從xcode安裝發佈自己開發的程式 (免付費開發者帳號)

注意:這只是拿來做程式開發測試用途,越獄(Jailbreak)後所產生的後果和問題要自行負責

 

首先,先感謝綠毒團隊 讓A5晶片(iPhone 4s / iPad2) 在iOS 5.01也成功越獄了
這下能夠釋放iDevices的全功能,讓iDevices更強大


好吧,這次的重點
是要讓自己寫的iOS程式能夠在實機底下執行做測試
畢竟有些功能模擬器還是無法達到的

若程式的規模不及到要上架的程度
可以使用這個方法來做測試

 

當然,有付費版的開發者帳號最佳 (USD$99/年),就別看此篇了
在開始之前,你需要先JB你的裝置
這可能需要你自行去Google,假設你已經完成越獄了
會有個Cydia的程式安裝在你的裝置上 

 

筆者的環境

Mac OSX 10.7.2 (Lion)

Xcode 4.2.1

iPad2 16G Wifi (iOS 5.01 Jailbreaken)

 

 

等下將會以iOS 5.01 做示範


Step1  到cydia安裝AppSync

 

打開Cydia,使用底下的搜尋框尋找Appsync

100APPLE_IMG_0049.PNG  

就會有很多的名稱叫AppSync for iOS5.0+,擇一安裝即可 (這需符合你裝置的iOS版本)

這裡只是以此為範例 


Step2  編輯SDKSettings.plist

 

打開Finder,找到左側欄的應用程式 > 工具程式 > 終端機

 

鍵入指令

sudo vi /Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS5.0.sdk/SDKSettings.plist

 

若是xcode 4.3版請修改

vi /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS5.1.sdk/SDKSettings.plist

(感謝Chiakie大大的幫忙)

Screen Shot 2012-01-23 at 下午1.52.00.png

輸入自己帳戶的密碼之後

會打開vim的介面
(vim共有三種模式 一般介面,編輯模式,指令列模式)

詳細vim操作可參照 鳥哥的文章
http://linux.vbird.org/linux_basic/0310vi.php

按下鍵盤的 i 可以編輯,編輯完畢按下鍵盤的 Esc

Screen Shot 2012-01-23 at 下午1.54.04.png

尋找CODE_SIGNING_REQUIRED,這個Key
將其底下的String從YES改成NO

 

編輯完畢按下鍵盤的Esc鍵,直接鍵入 :wq  在按鍵盤的 Enter (或return鍵)

Screen Shot 2012-01-23 at 下午2.39.06.png  

即可退出


Step3  修改Xcode專案的設定

 

打開你的Xcode,開啟你需要編輯的專案

Screen Shot 2012-01-21 at 下午5.14.25.png

依照圖片按到專案名稱,按下Info頁籤

找到Deployment Target,確認iOS Deployment Target是否是為 5.0  (這需符合你裝置的iOS版本)

 

再來相同的地方,找到Build Settings的頁籤

Screen Shot 2012-01-21 at 下午5.13.29.png

找到Code Signing

將以下全部改成Don’t Code Sign

(預設值是Debug > Any iOS SDK和Release >  Any iOS SDK的值為iPhone Developer)

 

 


Step4  連接你的iDevice (iPhone/iPad)

 

連接你的iPhone或iPad

按下專案視窗的右邊Organizer,找到Devices頁籤

Screen Shot 2012-01-23 at 下午12.39.36.png

可以找到你的裝置並且是亮綠燈的,且符合專案使用的iOS版本

 

 

回到專案視窗

  

按Scheme下拉框選擇你剛剛連接的裝置,按下Run

複製 -Screen Shot 2012-01-23 at 下午12.53.46.png  

就會開始編譯

 Screen Shot 2012-01-23 at 下午12.52.47.png

 


Step5  實機測試

 

編譯成功後

Screen Shot 2012-01-23 at 下午12.53.46.png  

會出現這樣

Error launching remote program: fail to get the task for process ……

的錯誤沒有關係

 

查看一下iPhone或iPad

100APPLE_IMG_0048.PNG  

你會發現他已經成功裝到你的機器上摟

 

 


參考資料

http://www.minwt.com/?p=2825

http://happyjc1106.blogspot.com/2011/12/xcode43.html

http://linux.vbird.org/linux_basic/0310vi.php