1. 程式人生 > >iOS 快速從OC過渡到Swift,由理論到實戰-OC和Swift混編

iOS 快速從OC過渡到Swift,由理論到實戰-OC和Swift混編

接上篇文章:iOS 快速從OC過渡到Swift,由理論到實戰-Swift基礎

OC和Swift混編

a. Swift 和 OC 的對映關係

1.png

Swift 相容來大部分 OC,當然還有一些 Swift 不能夠使用的,例如 OC 中的預處理指令,即巨集定義不可使用,雖然在目前4.2版本下,已經開始支援了少量的巨集,如

#if DEBUG
#else
#endif

這種簡單的預處理指令。Swift 中推薦使用常量定義,定義一些全域性的常量例項、全域性方法來充當 巨集 的作用。

我們直到,Swift 中的一些自定義基類可以是不繼承自 NSObject,這是 OC 呼叫這類的的時刻,就需要使用關鍵自 @objc 來標記,這樣就可以正常使用來。

Swift中還有很多小細節部分,待大家在進一步學習過程中慢慢體會。

b. Objc 呼叫 Swift

由於國內大部分還都是 OC 程式設計環境,所以通常是先學習 OC 下呼叫 Swift。接下來我演示以下 OC 下配置橋接檔案,以及 Objc 和 Swift 的相互呼叫。

當我們在專案中第一次新增不同語言的檔案時,Xcode就會詢問你是否建立橋接檔案,你只需要點選”YES”,Xcode就可以幫你完成橋接配置(注意:第二次不會提示你,即使你刪除來橋接檔案)。

這份橋接檔案是 Swift 呼叫 OC 檔案的時,匯入 OC 標頭檔案使用的。在橋接檔案中你可看到以下內容。

//  Use this file to import your target's public headers that you would like to expose to Swift.

當然你也可以手動建立橋接檔案,自己配置環境。

我們可以建立”專案名稱-Bridging-Header”一個 .h 檔案作為橋接檔案,然後為專案配置此檔案的路徑。

此檔案是存放 Swift 使用的 OC 標頭檔案,此小節先不管它。

在 OC 呼叫 Swift 情況下,是通過Swift生成的一個頭檔案實現的,這個標頭檔案是編譯器自動完成的,它會將你在專案是到的 Swift 檔案自動對映稱 OC 語法,其他並不需要開發關注,該檔名稱為 “專案名稱-Swift”,你可以在 Build Settings 裡搜尋 Swift 可以看到:

這裡還可以配置其他 Swift 的選項,如語言版本等。

接下來我們建立一個名為 “AViewController.swift” 的控制器,我們現在要在 OC 檔案使用到該檔案,首先,我們匯入”專案名稱-Swift.h”的檔案,不會提示,需要你手動寫入(可以按住Command點選標頭檔案檢視)。

在 OC 的 “ViewController.h” 檔案中,我們進行跳轉控制器,這個控制器的使用和使用 OC的類沒有任何卻別,系統的”專案名稱-Swift.h”檔案已經幫我完成了對映。

#import "ViewController.h"
#import "TestDemo-Swift.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.title = @"OC頁面";
    self.view.backgroundColor = [UIColor whiteColor];
}

-(void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    AViewController *ctrl = [AViewController new];
    [self.navigationController pushViewController:ctrl animated:YES];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

c. Swift 呼叫 Objc

前面提到了 “TestDemo-Bridging-Header.h” 檔案,這個檔案中儲存的是用於供給 Swift 使用的 OC 標頭檔案。

我們將 OC 控制器檔案 “ViewController.h” 匯入該檔案,這樣我們在 Swift 檔案中就可以使用該檔案。

import UIKit

class AViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        self.title = "Swift頁面";
        self.view.backgroundColor = UIColor.red
    }

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        let ctrl = ViewController()
        self.navigationController?.pushViewController(ctrl, animated: true)
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}

上面介紹了在 OC 工程中建立 Swift 檔案,其實在 Swift 工程中建立 OC 檔案是類似的,大家可以嘗試一下。

Swift中的’巨集’和工具類

通過前面的學習,我們已經對 Swift 的語法,和 OC 進行混編等知識有了一定的瞭解,下面演示一些在專案中可能使用到的工具類。

我們知道,在目前的Swift中,還不能夠呼叫OC中的巨集定義,這些東西勢必需要我們重新寫過。在Swift中,所對應OC的巨集,只不過是一些全域性常量或者方法。下面列舉一些我的在OC專案中對映多來的”巨集”。

// MARK: -------------------------- 適配 --------------------------
/// 螢幕寬度
let kScreenHeight = UIScreen.main.bounds.height
/// 螢幕高度
let kScreenWidth = UIScreen.main.bounds.width

/// 狀態列高度
let kStatusBarHeight = UIApplication.shared.statusBarFrame.size.height
/// 導航欄高度
let kNavigationbarHeight = (kStatusBarHeight+44.0)
/// Tab高度
let kTabBarHeight = kStatusBarHeight > 20 ? 83 : 49

/// 螢幕比率
let kScreenWidthRatio = (kScreenWidth/375.0)
/// 調整
func AdaptedValue(x:Float) -> Float {
    return Float(CGFloat(x) * kScreenWidthRatio)
}
func ShiPei(x:Float) -> Float{
    return AdaptedValue(x: x)
}
/// 調整字型大小
func AdaptedFontSizeValue(x:Float) -> Float {
    return (x) * Float(kScreenWidthRatio)
}

// MARK: -------------------------- 顏色 --------------------------
// MARK: 請參考 UIColor_Extentsion.swift 檔案

// MARK: -------------------------- 偏好 --------------------------
func SaveInfoForKey(_ value:String ,_ Key:String) {
    UserDefaults.standard.set(value, forKey: Key)
    UserDefaults.standard.synchronize()
}
func GetInfoForKey(_ Key:String) -> Any! {
   return UserDefaults.standard.object(forKey: Key)
}
func RemoveObjectForKey(_ Key:String){
    UserDefaults.standard.removeObject(forKey: Key)
    UserDefaults.standard.synchronize()
}

// MARK: -------------------------- 輸出 --------------------------
// 帶方法名、行數
func printLog(_ message:T,method:String = #function,line:Int = #line){
    print("-[method:\(method)] " + "[line:\(line)] " + "\(message)")
}
// 只在Debug下輸出,為了給習慣OC輸出寫法的同事
func DLog(_ format: String, method:String = #function,line:Int = #line,_ args: CVarArg...){
//    print("-[method:\(method)] " + "[line:\(line)] ", separator: "", terminator: "")
    #if DEBUG
        let va_list = getVaList(args)
        NSLogv(format, va_list)
    #else
    #endif
}
// 只在Debug下輸出,為了為習慣PHP輸出寫法的同事
func echo(_ format: String,_ args: CVarArg...) {
    #if DEBUG
        let va_list = getVaList(args)
        NSLogv(format, va_list)
    #else
    #endif
}

使用【擴充套件】功能建立一些工具

顏色類

extension UIColor {
    // RGB顏色
    static func rgba(_ r:CGFloat,_ g:CGFloat,_ b:CGFloat,_ a:CGFloat) -> UIColor {
        return UIColor.init(red: (r)/255.0, green: (g)/255.0, blue: (b)/255.0, alpha: a)
    }
    static func rgb(_ r:CGFloat,_ g:CGFloat,_ b:CGFloat) -> UIColor {
        return rgba((r), (g), (b), 1)
    }
    /// 十六進位制的色值 例如:0xfff000
    static func hex(_ value:Int) -> UIColor{
        return rgb(CGFloat((value & 0xFF0000) >> 16),
                   CGFloat((value & 0x00FF00) >> 8),
                   CGFloat((value & 0x0000FF)))
    }
    /// 十六進位制的色值,例如:“#FF0000”,“0xFF0000”
    static func hexString(_ value:String) -> UIColor{
        var cString:String = value.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
        cString = cString.lowercased()
        if cString.hasPrefix("#") {
            cString = cString.substring(from: cString.index(after: cString.startIndex))
        }
        if cString.hasPrefix("0x") {
            cString = cString.substring(from: cString.index(cString.startIndex, offsetBy: 2))
        }
        if cString.lengthOfBytes(using: String.Encoding.utf8) != 6 {
            return UIColor.clear
        }
        var startIndex = cString.startIndex
        var endIndex = cString.index(startIndex, offsetBy: 2)
        let rStr = cString[startIndex..<endIndex]
        startIndex = cString.index(startIndex, offsetBy: 2)
        endIndex = cString.index(startIndex, offsetBy: 2)
        let gStr = cString[startIndex..<endIndex]
        startIndex = cString.index(startIndex, offsetBy: 2)
        endIndex = cString.index(startIndex, offsetBy: 2)
        let bStr = cString[startIndex..<endIndex]
        var r :UInt32 = 0x0;
        var g :UInt32 = 0x0;
        var b :UInt32 = 0x0;
        Scanner.init(string: String(rStr)).scanHexInt32(&r);
        Scanner.init(string: String(gStr)).scanHexInt32(&g);
        Scanner.init(string: String(bStr)).scanHexInt32(&b);
        return rgb(CGFloat(r), CGFloat(g), CGFloat(b))
    }
    /// 隨機色
    static var rand: UIColor {
        return rgb(CGFloat(arc4random_uniform(255)), CGFloat(arc4random_uniform(255)), CGFloat(arc4random_uniform(255)))
    }
}

字元轉數字類

extension String {
    /// 將字串轉換為合法的數字字元
    ///
    /// - Returns: String型別
    func toPureNumber() -> String {
        let characters:String = "0123456789."
        var originString:String = ""
        for c in self {
            if characters.contains(c){
                if ".".contains(c) && originString.contains(c){}
                else{
                    originString.append(c)
                }
            }
        }
        return originString
    }
    /// 將字串轉換為 Double 型別的數字
    ///
    /// - Returns: Double型別
    func toDouble() -> Double {
        return Double(self.toPureNumber())!
    }
    /// 將字串轉換為 Float 型別的數字
    ///
    /// - Returns: Float型別
    func toFloat() -> Float {
        return Float(self.toDouble())
    }
    /// 將字串轉換為 Int 型別的數字
    ///
    /// - Returns: Int型別
    func toInt() -> Int {
        return Int(self.toDouble())
    }

    // 保留舊版本的字元轉換為數字型別,例:"123".floatValue
    var pureNumber: String {
        return self.toPureNumber()
    }
    var doubleValue: Double {
        return self.toDouble()
    }
    var floatValue: Float {
        return self.toFloat()
    }
    var intValue: Int {
        return self.toInt()
    }
}

以上介紹了幾種工具類的建立方式,相信大家進過基礎一節過後,都能看懂。

實戰篇

在這一節中,我們利用之前的測試專案簡單的演示Swift下如何搭建頁面、和OC進行資料傳遞,其中會涉及到自定義資料模型、延遲載入、型別轉換、協議、協議代理模式、弱引用、閉包、閉包傳值、檢測釋放等等,希望能都幫助到第一次接觸 Swift 的小白們。

Demo中,簡單搭建一個Swift頁面,頁面中展示一個列表,給列表Cell有標題,自標題和一張圖片,使用者點選某個cell時,將資料回傳到上一個OC頁面。

a. 定義Model層

首先我們需要要一個自定義模型,以及一個用來獲取、處理資料的View-Model。

首先是自定義模型:

import UIKit
// 列表資料來源 
class AVTableModel: NSObject {
    var title: String?
    var subTitle: String?
    var image: String?
}

View-Model:

import UIKit

class AVTableManager: NSObject {
    // 定義資料請求,使用閉包回撥
    class func requestData(response:@escaping (_ flag : Bool,_ resArray : [AVTableModel]?) -> Void) {
        // 模擬資料
        var resList = [AVTableModel]()
        for i in 0...30 {
            // 使用自定義模型
            let avtm:AVTableModel = AVTableModel()
            avtm.title = "\(i) row"
            avtm.subTitle = [
                                "月光推開窗親吻臉頰,沁得芬芳輕叩門,斂去湖中婆娑影,拈起肩上落梨花",
                                "屋檐下的風輕輕拂過了衣角,弄皺了桌上的畫卷,月影疏疏,落花朵朵,不經意間,看成了風景",
                                "遠處的煙穿過水路,哼著不知名的小調,踏碎了一方的圓月",
                                "誰家的酒釀醉了空氣中潮溼的時光,睜眼瞬間,藏進了樓閣"
                            ][i%4]
            avtm.image = ["0.jpg","1.jpg","2.jpg"][i%3]
            resList.append(avtm)
        }
        // 回撥資料和當前狀態
        response(true, resList)
    }
}

b. 控制器

控制器需要完成UI的搭建,因此需要一個列表,完成相應的列表代理,再者就是定義資料,完成頁面互動邏輯。

Swift 中列表是使用和 OC 非常型別,僅僅是方法的實現和呼叫方面是鏈式呼叫。

import UIKit

class AViewController: UIViewController,UITableViewDelegate,UITableViewDataSource { // 實現列表的代理
    // 資料來源,延遲屬性
    lazy var dataArray = [AVTableModel]()
    // 列表檢視
    @IBOutlet weak var table: UITableView!
    override func viewDidLoad() {
        super.viewDidLoad()
        self.title = "Swift頁面";
        // 配置UI
        self.setUpUI()
        // 獲取資料
        self.getData()  // 獲取資料
    }

    // 配置UI
    func setUpUI() {
        self.table.tableFooterView = UIView()
    }

    // 獲取資料
    func getData() {
        // 這裡因為是類方法,不會產生迴圈引用
        AVTableManager.requestData { (flag, resList) in
            if flag {
                self.dataArray = resList!
                self.table.reloadData()
            }
        }
    }

    // tableView的代理
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.dataArray.count
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        // 建立cell,並返回給tableView
    }
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 60.5
    }

    // 檢測釋放
    deinit {
        print("\(self.classForCoder) 釋放了。。。")
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

}

c. 檢視層

我這裡使用的是 xib,使用方式和 OC 下並無多少差異,將一些必要的控制元件拉到類中做入口。

import UIKit
class AVTableViewCell: UITableViewCell {
    @IBOutlet weak var titleLabel: UILabel!      // 標題
    @IBOutlet weak var subTitleLabel: UILabel!   // 子標題
    @IBOutlet weak var imgView: UIImageView!     // 圖片
}

在控制器中完成列表cell的註冊,以及cell的賦值

import UIKit

class AViewController: UIViewController,UITableViewDelegate,UITableViewDataSource {
    // 資料來源,延遲屬性
    lazy var dataArray = [AVTableModel]()
    // 列表檢視
    @IBOutlet weak var table: UITableView!
    let cellId = "cellId"

    override func viewDidLoad() {
        super.viewDidLoad()
        self.title = "Swift頁面";
        // 配置UI
        self.setUpUI()
        // 獲取資料
        self.getData()  // 獲取資料
    }
    // 配置UI
    func setUpUI() {
        self.table.tableFooterView = UIView()
        // 註冊cell
        self.table .register(UINib.init(nibName: "AVTableViewCell", bundle: nil), forCellReuseIdentifier: cellId)
    }
    // 獲取資料
    func getData() {
        // 這裡因為是類方法,不會產生迴圈引用
        AVTableManager.requestData { (flag, resList) in
            if flag {
                self.dataArray = resList!
                self.table.reloadData()
            }
        }
    }
    // tableView的代理
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.dataArray.count
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        // 將UITableViewCell向下轉換為AVTableViewCell
        let cell: AVTableViewCell = table.dequeueReusableCell(withIdentifier: cellId) as! AVTableViewCell
        let model = self.dataArray[indexPath.row]
        // 將資料賦值給cell
        cell.titleLabel.text = model.title
        cell.subTitleLabel.text = model.subTitle
        cell.imgView.image = UIImage.init(named: model.image!)
        return cell
    }
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 60.5
    }
    deinit {
        print("\(self.classForCoder) 釋放了。。。")
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}

d. 將資料回傳到 OC 頁面

這裡演示代理和閉包兩種方式

首先是代理,和 OC 一樣,我們需要先定義一個協議用來約束遵循者完成該協議方法。

import Foundation
@objc protocol AViewControllerDelegate {
    // 預設是必須實現的,這裡將其定義為可選型別
    @objc optional func aViewCtrlData(model: AVTableModel) -> Void
}

那麼在控制器中,代理和閉包的宣告就可以想下面那樣

// 代理,將資料傳遞迴其他類,可選型別
weak var delegate: AViewControllerDelegate?
// 閉包,將第一條資料傳遞迴其他類, 該閉包和代理一樣是可選型別
var returnModelBlock: ((_ model: AVTableModel) ->Void)?

我們在使用者點選事件中將對應資料回傳到 OC 頁面中去

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        // 我們將前十項的資料使用代理方法回傳,剩餘的資料使用閉包方式回傳
        if indexPath.row > 10 {
            // 將資料通過代理回傳給 OC 頁面
            let model = self.dataArray[indexPath.row]
            self.delegate?.aViewCtrlData!(model: model)
        }
        else {
            // 將資料回傳給其他類
            if returnModelBlock != nil {
                returnModelBlock!(self.dataArray.first!)
            }
        }

        // 返回
        self.navigationController?.popViewController(animated: true)
    }

f. 在 OC 頁面中,使用 Swift 頁面中的block和代理和平常沒有什麼區別

AViewController *ctrl = [AViewController new];
ctrl.delegate = self;
// 這裡並沒有迴圈引用,因為self並沒有持有ctrl,ctrl僅是被self.navigationController所持有
ctrl.returnModelBlock = ^(AVTableModel * model) {
        [self aViewCtrlDataWithModel:model];
};
[self.navigationController pushViewController:ctrl animated:YES];

演示:

注:由於 OC 和 Swift 混編是由橋接檔案實現橋接的,因此並不是你寫完一個public屬性在OC中就能夠使用的,在使用前,你應該手動 command + B 進行編譯完成橋接檔案的內容。你可以使用 command + 點選TestDemo-Swift.h檔案 進入橋接檔案檢視橋接內容。

宣告

上述部分內容參考自: 

Swift 4.0 教程 – Swift程式設計 

Kenshin Cui’s Blog

作者:LOLITA0164