Swift 程式語言

QR Code 產生器 App:以 Core Image 濾波器製作QR Code條碼

QR Code 產生器 App:以 Core Image 濾波器製作QR Code條碼
QR Code 產生器 App:以 Core Image 濾波器製作QR Code條碼
In: Swift 程式語言

我在 Appcoda 曾經寫過一篇文章,是關於如何在 iOS 中建立 QR Code (二維條碼)閱讀器的教學。當時那篇文章的程式是用 Objective-C 撰寫而成的,因為 Swift 是後來才有的。隨後,我的好友 Simon 寫了一篇新的文章,針對 Swift 這個新語言的廣大粉絲進行全面改寫。等到這個主題的首篇文章發表出來時,基於廣告和行銷等目的之 QR Code 已經逐漸開始被廣泛運用,於是許多開發者都忙於開發自己的 QR Code 閱讀器。 QR Code 在今日已經無所不在;在報章雜誌、電視、 T 恤、廣告標語、網站上(這份清單還可以更長)都看得到其蹤影。另一方面,在 App Store 上面也有數不清的 QR Code 閱讀器 App 。你一定也知道,廣告主和開發者(不限這兩種人)都對 QR Code 很感興趣,因為它很新奇也很好管理。

經過了一段時間,也間隔了好多篇文章,現在透過本文,我又回過頭去探討 QR Code ,只是這回的目標完全不一樣了。我並不打算再講一次 QR Code 閱讀器;畢竟,這樣的文章我們已經寫過了(請參考上面的連結)。這回我打算示範如何打造 QR Code 產生器。相信我,你將會發現這一切就跟打造 QR Code 閱讀器一樣容易(甚至更容易)。但是首先,請容我先告訴你一些你應該知道的事情。

在 iOS 7 之前,要打造 QR Code 產生器是件非常困難的事。找得到的資源並不多,而大多數的開發者也總是避免自行撰寫程式碼;最簡單的作法,就是挑選 2 到 3 種不同的現有程式庫,然後套用到自己的 App 當中。萬分慶幸的是,那些都已成為過去。從 iOS 7 開始, Apple 讓所有與 QR Code 有關的工作都變得十分容易上手。要讀取 QR Code 的話,開發者們可以使用 AVFoundation 框架。至於要產生 QR Code ,開發者們只需要使用 Core Image 框架(更精確地說,是使用 Core Image 濾波器)即可。

為了讓 QR Code 更清晰可見,從 iOS 7 開始, QR Code 都是使用名為 CIQRCodeGenerator 的特殊濾波器( Filter )來產生 CIImage 物件( CoreImage 圖片)。只需要提供你想要轉換成 QR Code 圖片的資料, iOS 就會接手幫你做完剩下的繁重工作。傳回的圖片要怎麼處理都由你自己決定,只要別破壞圖片導致條碼無法判讀就行了。

在本文接下來的幾個小節中,你將學會如何快速簡便地製作 QR Code ,此外也會學到一些讓 QR Code 更清晰可見的技巧。在閱讀完本文之後,你將能夠在 iOS 中產生 QR Code ,並且縮放成任何你想要的尺寸。稍後你將會看到,在建立完 QR Code 之後,還必須稍加處理一下,好讓條碼能夠在輸出的尺寸中看起來更清楚。請留意,我即將介紹的步驟並非唯一的作法;那只是我個人偏好的作法,而確實也有許多其他的方式可以達到相同的目的。

講了這麼多,現在就讓我們進入正題吧。

範例 App 簡介

或許你已經猜到了,本文的範例 App 將不會太過複雜。下列的截圖可以讓你一睹我們即將實現的功能:

qr-code-generator

相較於其他文章的範例 App ,這回我們將從頭開始建造整個範例 App ,因為步驟並不困難。我們的介面可以拆分成下列幾個子視圖:

  • 一個文字欄位,用來輸入想要轉換成 QR Code 的任何文字或網址。
  • 一個按鈕,同時用來產生及刪除 QR Code 。
  • 一個圖片視圖,用來顯示 QR Code 。
  • 一個滑桿,用來縮放最終的結果。

請記住,產生的 QR Code 是一張 CoreImage 圖片( CIImage ),所以我們會在適當時刻將之轉換成 UIImage 物件。除此之外,在將它設定為圖片視圖之前尚需要進行一些處理作業,這些細節請容我稍後再做說明。

如果你已經準備好了,請開啟 Xcode ,讓我們開始來打造全新的 App 吧。在做出第 1 個 QR Code 之前,還差 2 個步驟。

打造 UI

第 1 步就是建立實際的專案,所以請開啟 Xcode ,並且確定選好了所需的項目。在導覽精靈中,首先選取 Single View Application 範本。接著繼續將 App 的名稱設定為 QRCodeGen ,此外也別忘了檢查是否將語言選定為 Swift 。做完這些之後,找個地方來存放此專案,這個步驟就完成了。

第 2 步才是真正與設定使用者介面( UI )有關的部分。所以請來到 Project Navigator ,點擊 Main.storyboard 檔案以便開啟 Interface Builder 。下列的截圖說明了 ViewController 場景在新增完所有必要的子視圖之後應該要長什麼樣子:

t35_2_scene_sample

讓我們從 Object Library 拖曳 UITextField 物件並且放置到預設的視圖上開始。設定其外框為如下所示:

  • X = 16
  • Y = 28
  • Width = 568
  • Height = 30 (無法修改)

文字欄位目前這樣設定就可以了。我們稍後會再跟其他要加入的子視圖一起設定約束。

現在,在場景中加入 UIButton 。設定如下的屬性:

  • Title = “Generate”
  • Text Color = White
  • Background(十六進制) = #F39C12

並且設定其外框:

  • X = 464
  • Y = 66
  • Width = 120
  • Height = 30

接著,拖曳一個 UIImageView 物件並且新增到視圖中。我們只需要設定其外框:

  • X = 200
  • Y = 200
  • Width = 200
  • Height = 200

最後,加入一個 UISlider 物件。設定其屬性:

  • Minimum 數值 = 0
  • Maximum 數值 = 2
  • Current 數值 = 1
  • Min Track Tint(十六進制) = #C0392B
  • Thumb Tint(十六進制) = #D35400
  • Hidden = true(勾選)

並且設定其外框:

  • X = 189
  • Y = 550
  • Width = 223
  • Height = 31 (無法修改)

現在讓我們來設定約束。我們只會設定其中幾個元件,然後讓 Xcode 為我們把缺漏的部分補齊。首先選取圖片視圖,讓我們從它開始。接著點擊右下角的 Pin 按鈕,並勾選 WidthHeight 約束。我們希望圖片視圖永遠保持相同的尺寸。

t35_3_pin_button

接著選取的還是圖片視圖,點擊 Align 按鈕,然後勾選 Horizontal Center in Container (在容器中左右置中)和 Vertical Center in Container (在容器中上下置中)約束選項。

t35_4_align_button

圖片視圖的約束已經準備就緒,所以現在輪到選取滑桿,並點擊 Pin 按鈕。設定就跟圖片視圖一樣,同樣是勾選 WidthHeight 約束。接著,點擊 Align 按鈕,而且只勾選 Horizontal Center in Container 選項。

在做完上述這些設定之後,我們讓 Xcode 自動幫忙補齊缺漏的約束。首先選取 ViewController 場景物件,如下圖所示:

t35_5_scene_sample

接著點擊右下角的 Resolve Auto Layout Issues (解決 Auto Layout 問題)按鈕(就在 Pin 按鈕隔壁),然後在彈出的功能表中,選取位於 All Views in View Controller ( View Controller 中的所有視圖)區段的 Add Missing Constraints (加入缺漏的約束)選項。

最後,讓我們來宣告並連接幾個 IBOutlet 屬性和 IBAction 函式吧。首先,開啟 ViewController.swift 檔案並加入下列的屬性:

@IBOutlet weak var textField: UITextField!

@IBOutlet weak var imgQRCode: UIImageView!

@IBOutlet weak var btnAction: UIButton!

@IBOutlet weak var slider: UISlider!

從它們的名稱和資料類型,大致可以理解每個屬性所對應的子視圖。所以請回到 Interface Builder 中完成連結作業吧(我相信你知道怎麼做,所以細節我就不詳述了)。

此外我們還需要 2 個 IBAction 函式,所以請回到 ViewController.swift 檔案,並且定義如下:

@IBAction func performButtonAction(sender: AnyObject) {

}


@IBAction func changeImageViewScale(sender: AnyObject) {

}

現在最後一次使用 Interface Builder ,把上述的函式連結到合適的子視圖。第 1 個函式必須連結到 Generate 按鈕的 Touch Up Inside 事件,而第 2 個函式則必須連結到滑桿的 Value Changed 事件。

在做完上述這些設定之後,便準備好可以開始來撰寫程式碼了。我們在 UI 這部分的工作已經結束,現在讓我們來看一下如何產生 QR Code 吧。

產生 QR Code

從剛才做的這些事,你很容易可以猜到我們將使用 performButtonAction(_:) IBAction 函式來產生新的條碼。不過在這之前,讓我們先在類別開頭宣告一個新的變數:

var qrcodeImage: CIImage!

我們將使用上述的變數來儲存產生的 QR Code 圖片。別忘了我們將使用 Core Image 濾波器來產生條碼,所以將會是一張 CoreImage 圖片。稍後我們會將它轉換成 UIImage ,不過現在先這樣就夠了。

現在,讓我們來到 performButtonAction(_:) IBAction 函式。此函式有 2 個用途:既會產生新的 QR Code ,又會移除畫面上與程式內的舊條碼圖片。我們可以在此 IBAction 函式的本體中透過 qrcodeImage 屬性是否為 nil 來簡單分辨這 2 種不同的情況。此外,按一下按鈕,就會將按鈕的標題從「 Generate 」(產生)變成「 Clear 」(清除),再按一次則會相反過來。

讓我們來逐步檢視每一行程式碼,並且關注產生 QR Code 的部分。使用者如果沒有輸入文字的話,顯然就不應該建立任何的 QR Code ,所以我們必須確保這點。馬上你就會看到一個迴避此情況的簡單作法,不過在真實的 App 當中,你最好能夠對使用者再更友善一些:

@IBAction func performButtonAction(sender: AnyObject) {
    if qrcodeImage == nil {
        if textField.text == "" {
            return
        }
    }
}

我們知道,假使文字欄位是空白的,就什麼事也不會發生。

現在讓我們來看如何產生 QR Code 。如同我在引言中所說的,我們需要做的是建立新的 CoreImage 濾波器(利用 CIQRCodeGenerator )來指定一些參數,然後即可獲得輸出的圖片,也就是 QR Code 圖片。需要指定的 2 個參數如下:

  1. inputMessage:這是要轉換成 QR Code 圖片的初始資料。事實上,此參數必須是 NSData 物件,所以請確定你所使用的字串或其他物件都已轉換成這種資料類型。
  2. inputCorrectionLevel:這裡表示有多少額外的錯誤更正資料要被附加到輸出的 QR Code 圖片中。其數值是 4 種字串之一: LMQH ,分別對應到不同的錯誤復原能力,依序為 7% 、 15% 、 25% 、 30% 。數值越大,輸出的 QR Code 圖片也就越大。

下列是完整的程式碼:

@IBAction func performButtonAction(sender: AnyObject) {
    if qrcodeImage == nil {
        ...

        let data = textField.text.dataUsingEncoding(NSISOLatin1StringEncoding, allowLossyConversion: false)

        let filter = CIFilter(name: "CIQRCodeGenerator")

        filter.setValue(data, forKey: "inputMessage")
        filter.setValue("Q", forKey: "inputCorrectionLevel")

        qrcodeImage = filter.outputImage
    }
}

要產生蘊含了文字欄位資料的 QR Code ,你只需要依靠上述這 5 行程式碼就夠了。請留意, Apple 建議使用 NSISOLatin1StringEncoding 而不是 NSUTF8StringEncoding ,不過後者使用上也沒什麼問題就是了。

現在,讓我們來測試此 App ,並且觀察看看發生了什麼事。在執行此 App 之前,先在 IBAction 函式中加入下列這幾行程式碼:

@IBAction func performButtonAction(sender: AnyObject) {
    if qrcodeImage == nil {
        ...

        imgQRCode.image = UIImage(CIImage: qrcodeImage)

        textField.resignFirstResponder()
    }
}

我們只是將輸出的 CIImage 轉換成 UIImage 物件,接著透過呼叫文字欄位的 resignFirstResponder() 來隱藏鍵盤。

在新增完上述的程式碼之後,就可以執行 App 了。你可以在模擬器或實機上執行。在 App 啟動後,來到文字欄位,並且輸入你想要的文字,或是如下圖所示的網址字串。接著點擊 Generate 按鈕,這樣 QR Code 圖片就會被產生出來了:

QR Code

恭喜,你剛才成功建立了首張 QR Code 圖片!

根據你要轉換的字串的長度,以及你所設定的輸入更正等級,所產生的 QR Code 的密度也會不同。不過這並非目前最重要的事。真正重要的是 QR Code 圖片的尺寸沒有等於圖片視圖的尺寸。之所以會得到如上圖所示的尺寸,是因為圖片視圖的 Content Mode (內容模式)的預設值是 Scale To Fill (縮放以填滿),於是導致圖片變得模糊不清。不僅如此;輸入的資料越少, QR Code 圖片就越模糊。這顯然不是我們想要獲得的最終結果,幸好有不少能夠修正此問題的作法。怎麼做呢?請稍微有點耐性吧,因為那是我們在下一個小節才要介紹的內容;現在我們還有其他幾件收尾的事情要做。

如前所述,我們希望 Generate 按鈕能夠具備雙重角色。這意味著它不僅能夠產生 QR Code 圖片(就正是它目前的功能),還要可以從畫面上和 qrcodeImage 屬性中移除圖片。所以讓我們在 IBAction 函式中加入缺漏的程式碼吧,同時也讓該按鈕能夠重新建立 QR Code 。程式碼如下所示:

@IBAction func performButtonAction(sender: AnyObject) {
    if qrcodeImage == nil {
        ...

        btnAction.setTitle("Clear", forState: UIControlState.Normal)        
        slider.hidden = false
    }
    else {
        imgQRCode.image = nil
        qrcodeImage = nil
        btnAction.setTitle("Generate", forState: UIControlState.Normal)
    }
}

在判斷式的第 1 個情況中,將會產生新的 QR Code ,我們加入一行新的程式碼,用來將按鈕的標題從「 Generate 」變更成「 Clear 」,同時也讓滑桿出現(在這之前滑桿都是隱藏起來的)。而在第 2 個情況中,將會移除現有的 QR Code 圖片,所以我們採取了對應的動作。請留意,最後我們再度變更了按鈕的標題。

如果你再次嘗試執行此 App ,將會發現自己能夠無限制地產生和清除 QR Code 圖片。所以請試著把玩看看,先不用去管圖片模糊的問題;幾分鐘之後我們就會把這個問題搞定的。

修正模糊圖片

我們接下來要處理的問題是,如何讓產生的模糊 QR Code 圖片變得清晰,同時又剛好能夠縮放成適合圖片視圖的尺寸。不過我們必須非常小心,因為我們不應該更動 QR Code 圖片,否則條碼有可能會無法辨讀。最終結果必須維持原樣。能夠採取的方法並不多,在此我將展示一個非常簡單的解決方案。當然還有其他許多作法,比方說使用 CoreGraphics 程式庫,不過我把那些功課留給你自己去嘗試。我個人覺得我即將提供的是最快速的作法。

基本上,要將 QR Code 圖片放大而又不致於變得模糊,就必須改變其 transform (轉換)屬性。但是因為無法同時將 QR Code 圖片放大然後又要新增到圖片視圖中,所以我們建立了另一個要用來放大的 CIImage ,然後將之指派給圖片視圖。

這樣會有一個問題。原始的 QR Code 圖片應該放大多少呢?我們曉得 QR Code 圖片尺寸會因內容和輸入修正等級數值而不同,所以我們要如何將微調作業最佳化呢?事實上,要找出上述問題的答案並沒有那麼困難。預期的縮放係數永遠等於圖片視圖寬度(或高度)與原始 QR Code 圖片寬度(或高度)相除的比例。以程式設計的角度來看, X 軸與 Y 軸的縮放係數如下:

let scaleX = imgQRCode.frame.size.width / qrcodeImage.extent().size.width
let scaleY = imgQRCode.frame.size.height / qrcodeImage.extent().size.height

extent() 函式會傳回圖片的外框。

請將上述的計算牢記在心,現在讓我們繼續來建立名為 displayQRCodeImage() 的新自訂函式。我故意使用新的自訂函式,而不是將程式碼新增到 IBAction 函式中,因為這樣比較容易把焦點放在我們要做的事情上面。

在下列的實作中,我們將會執行 3 項任務:

  1. 我們會為 X 軸和 Y 軸指定縮放係數(就是前面看過的那段程式碼)。
  2. 我們會建立一個新的 CIImage 來做為第一張圖片(放大的圖片)的轉換結果。
  3. 我們會將新的圖片轉換成 UIImage 物件,最終將之指派給圖片視圖。
func displayQRCodeImage() {
 let scaleX = imgQRCode.frame.size.width / qrcodeImage.extent().size.width
 let scaleY = imgQRCode.frame.size.height / qrcodeImage.extent().size.height

 let transformedImage = qrcodeImage.imageByApplyingTransform(CGAffineTransformMakeScale(scaleX, scaleY))

imgQRCode.image = UIImage(CIImage: transformedImage)


}

上述實作主要仰賴的是 CIImage 類別的 imageByApplyingTransform(_:) 函式。此函式實際上將會透過轉換我們所提供的既有圖片,來建立並傳回新的圖片。

上述所實作的函式內容(老實說,其實只需要一行而已)就能夠讓我們將 QR Code 圖片變得清晰,並且避免產生模糊的結果。現在,在測試此 App 之前,我們還差一個步驟,才能夠返回 performButtonAction(_:) IBAction 函式並且呼叫此函式。回到 IBAction 函式的最開頭,找到下列這行,並且予以刪除:

imgQRCode.image = UIImage(CIImage: qrcodeImage)

接著只需要呼叫新的自訂函式就可以了:

@IBAction func performButtonAction(sender: AnyObject) {
    if qrcodeImage == nil {
        ...

        displayQRCodeImage()
    }
    ...
}

我們已經準備好可以再度測試此 App 了。執行看看,輸入一些文字,然後使用 Generate 按鈕來建立新的 QR Code 圖片。如同你在下圖所見,這回輸出的圖片清晰許多,模糊的情況已不復見。

qr-code-generator

不只一種尺寸

到目前為止,關於產生 QR Code 圖片所必須知道的一切你已經都了然於胸。但是我有一些議題還沒有講完,因此最後我想示範一件東西,一件我被詢問過無數次的東西。事實上,我要講的就是如何動態產生不同尺寸的 QR Code ,而不是只有圖片視圖上面看到的那種固定尺寸而已。

在上一個小節中,你已經看過我們如何處理原始 QR Code 圖片的放大,好讓它能夠符合圖片視圖而不致於顯得模糊。當時的基本概念就是變更圖片的轉換屬性並且產生新的、放大的圖片,最終透過一些技巧我們得到了清晰的條碼。在本小節中,我們將根據相同的概念,但是這回我們不再關注於 QR Code 圖片。相反地,我們將改變圖片視圖的縮放,這麼做的話, QR Code 也將跟著被「重新調整」其尺寸。

透過使用視圖下方現有的滑桿,我們的範例 App 將會變得更為有趣一些。假使你還記得的話,在製作 UI 期間,我們將最小值設定為 0 ,而將最大值設定為 2 。我們將預設值設定為 1 ,實際上代表的正是目前圖片視圖的縮放係數(在預設的情況下,任何視圖都不會被放大或縮小,所以轉換的縮放係數等於 1 )。我們要做的就是,讓滑桿往左可以縮小圖片視圖,然後滑桿往右則是放大。當滑桿來到最大值時,圖片視圖看起來會有 2 倍大,也就是說 QR Code 看起來將會比較大。

先前我們已經定義了如下的 IBAction 函式:

@IBAction func changeImageViewScale(sender: AnyObject) {

}

此函式會在每次滑桿數值發生變化時被呼叫到,而這正是我們縮放圖片視圖時所需要的。因為滑桿的數值就跟圖片視圖的縮放倍率相同,所以我們可以直接使用此數值。如你所見,只需要一行程式碼而已:

@IBAction func changeImageViewScale(sender: AnyObject) {
    imgQRCode.transform = CGAffineTransformMakeScale(CGFloat(slider.value), CGFloat(slider.value))
}

請留意滑桿的數值是 Float 浮點數型態,而 CGAffineTransformMakeScale(...) 函式接受的則是 CGFloat 參數,所以必須進行轉型( Casting )。

如果你現在執行此 App 的話,將會看到每次在產生 QR Code 並且移動了滑桿之後,圖片視圖就會跟著放大或縮小。這種能力在你想要顯示不同尺寸的 QR Code 時將會非常好用,而且在縮放的同時完全不會破壞條碼本身所代表的資料。

t35_7_qrcode_scaledup

最後的調整

現在一切運作良好,但是仍舊有些事情困擾著我們。一、在刪除 QR Code 之後,滑桿並不會消失。二、即便已經建立了 QR Code ,文字欄位仍是可以編輯的,導致鍵盤一直擋在螢幕上。除此之外,完全沒有辦法立刻隱藏滑桿,因為先前已經修改過按鈕的功能了。

幸運的是,只需要在 performButtonAction(_:) IBAction 函式中撰寫 2 行程式碼即可同時解決上述的 2 個問題。回到程式碼的最開頭,找到並刪除下列這行:

slider.hidden = false

接著,在上述的 IBAction 函式的最後(在 else 區塊之後),加入下列這 2 行程式碼:

@IBAction func performButtonAction(sender: AnyObject) {
    ...

    textField.enabled = !textField.enabled
    slider.hidden = !slider.hidden
}

有了這 2 行,當按鈕被點擊時,文字欄位將會切換成可編輯或不可編輯,而滑桿則會切換成隱藏或看得見。新增了這項微小的改進,讓我們的範例 App 變得更加完美。你可以再次嘗試執行看看,以便確認運作起來一如預期般正常。

結語

本文已經進入尾聲,誠摯地希望我們在本文中逐步打造的簡單 App 能夠幫助你建立自己的 QR Code 。當你體驗完如何製作之後,將會發現整個過程一點也不困難。為了達到最佳效果(事實上也為了能夠獲得正確的結果),請確定你所提供的要做為 QR Code 來源的資料不會過大,因為一旦提供太過龐大的資料,可能什麼結果也得不到。無論如何,下次你再被要求必須在 App 中整合產生 QR Code 的功能時你就知道做了,一切作法盡在本文中。製作 QR Code 真是樂趣無窮呀!

供你參考,請從這裡下載本文的完整專案。

譯者簡介:陳佳新 – 奇步應用共同創辦人,開發自有 App 和網站之外,也承包各式案件。譯有多本電腦書籍,包括 O’Reilly 出版的 iOS 、 Android 、 Agile 和 Google Cloud 等主題,也在報紙上寫過小說。現與妻兒居住在故鄉彰化。歡迎造訪 https://chibuapp.com ,來信請寄到 [email protected]

原文Building a QR Code Generator with Core Image Filters

作者
Gabriel Theodoropoulos
資深軟體開發員,從事相關工作超過二十年,專門在不同的平台和各種程式語言去解決軟體開發問題。自2010年中,Gabriel專注在iOS程式的開發,利用教程與世界上每個角落的人分享知識。可以在Google+或推特關注 Gabriel。
評論
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。