iOS/Swift Tips 1

1.重寫hitTest方法,干預iOS事件傳遞過程

以下所示,view上有一個button,button一半的frame在父類view bounds以外, 按照iOS系統默認的處理邏輯, 若是點擊按鈕上半部分,則按鈕不會響應時間,若是點擊下半部分才行, 要想讓點擊上半部分同樣相應事件,則須要干預事件的傳遞過程,以下代碼所示. 判斷事件發生的point在button上面,則讓button去響應事件便可.python

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        let btnPoint = self.convert(point, to: navigationButton)
        if navigationButton.point(inside: btnPoint, with: event) {
            return navigationButton
        }else{
            return super.hitTest(point, with: event)
        }
    }

2.清空WKWebView的歷史紀錄

項目需求須要清空webView的歷史紀錄,要否則只能使用兩個webView, 按理說應該是一個就能解決的, 用兩個內心有點不爽. 百度兩個多小時找不到可用的方法, 最終在stackoverflow上面找到一種解決方案, 使用webWKWebView的私有方法, 代碼以下所示:ios

answer in stackoverflow程序員

webView.backForwardList.perform(Selector(("_removeAllItems")))

3.UIScrollView添加約束

//1.把scrollView添加到控制器view
        let scrollView = UIScrollView()
        view.addSubview(scrollView)
        scrollView.snp.updateConstraints { (make) in
            make.edges.equalToSuperview()
        }
        //2.給scrollView添加一個containerView
        let containerView = UIView()
        scrollView.addSubview(containerView)
        containerView.snp.makeConstraints { (make) in
            make.edges.equalTo(scrollView)
            make.width.equalTo(scrollView)
        }
        //3.全部的子控件都放到containerView裏面, 在最後一個子控件後設置約束
        containerView.snp.makeConstraints { (make) in
            make.bottom.equalTo(goButton.snp.bottom).offset(20)
        }

4.swift中從nib建立一個控制器

extension UIViewController {
    static func createFromNib() -> Self {
        let className = "\(type(of: self))".split(separator: ".").first
        assert(className != nil, "\n\n \(type(of: self)) -> 找不到對應NIB,請檢查nibName是否綁定Class \n\n")
        return self.init(nibName: String(className!), bundle: Bundle.main)
    }
}

5.代碼結束應用進程: exit(0)

6.獲取應用的惟一標識

早期的ios版本, 能夠獲取到udid, mac address等信息, 可是ios8以後的版本都獲取不到了, UUID雖然能夠保證惟一性, 可是沒法保證每次獲取到的相同. IDFA也能夠再不少時候保證惟一性, 可是大概ios10以後, 用戶能夠選擇關閉IDFA追蹤, 這時候, 就沒法獲取到正確的IDFA了. 在網上找了很多的解決方案, 其中最靠譜的是應用第一次打開時候生成惟一的一個UUID,並存儲到SSKeyChain中. 而SSKeyChain中的數據能夠保證每次重啓/重裝/應用升級/系統升級都不發生變化. 有說惟一會致使SSKeyChain發生變化的是重置系統和resetchain方法重置KeyChain.web

7.自定義鍵盤

自定義鍵盤實際上是想當簡單的, 自定義鍵盤主要是用到了UITextView的inoutView屬性, 當給UITextField賦值inputView屬性時候, 若是textField得到輸入焦點, 則會彈出你設置的鍵盤View. 至於你想在鍵盤上畫什麼東西, 那就是你的事情了, 隨便畫, 就是這麼簡單, 沒什麼好說的.shell

let rect = CGRect(x: 0, y: 0, width: 0, height: 200)
let keyBoardView = CustomKeyBoardView(frame: rect)
textField.inputView = keyBoardView

8.打開應用的權限設置頁面

// let url = URL(string: UIApplicationOpenSettingsURLString)
// UIApplication.shared.openURL(url!)

實現自定義鍵盤的另外一種方案是使用Custom Keyboard Extension, 這裏不作詳細的探究.swift

9.swift中的預編譯選項

Swift 中沒有宏的概念,可是提供了 Active Compilation Conditions ,這個設置能夠替代以前預編譯宏的方式,來作本身的條件編譯.閉包

//swift支持的預編譯格式
#if <condition>
#elseif <condition>
#else
#endif

//1.檢測系統型號或者系統內核類型
// os()只能檢測系統類型,而沒法檢測系統的版本, OSX, iOS
//arch()檢測處理器的型號,x86_64, arm, arm64, i386
#if os(OSX)
    typealias Color = NSColor
#elseif os(iOS)
    typealias Color = UIColor
#endif

//2.檢測是Debug版本仍是Release版本
#if DEBUG
#elseif Release
#else
#endif

//3.檢測swift語言的版本
#if swift(>=3.2)
    // Swift 3.2 及以上
#else
    // Swift 3.2 如下
#endif

//4.檢測iOS系統版本, 不一樣的系統執行不一樣的代碼
if #available(iOS 9.0, *) {
    // iOS 9 及以上
}else{
    // iOS 9 如下
}

swift中也能夠自定義編譯類型, 並在本身的代碼中使用本身的類型, 能夠再以下所示的位置設置自定義類型.app

10.Swift註釋中的TODO & FIXME & ERROR

Xcode也給咱們提供了三種實用的簡易標記,即 MARK、TODO、FIXME,如今這些在 Objective-C 或者 Swift 環境下都是可使用的。須要注意的是 MARK、TODO、FIXME 均必須大寫,Xcode將會在代碼中尋找這樣的註釋,而後以粗體標籤的形式將名稱顯示在導航欄,就如同咱們會用 「#pragma mark -」 符號來標記代碼區間同樣的道理。async

TODO, MARK, FIXME的使用方法以下所示:ide

//TODO: 標記未來要完成的內容
//MARK: 標記一件事情
//FIXME: 標記之後要修正或完善的內容
//ERROR:標記一段錯誤

然而, 問題來了, 雖然可以標記, 可是若是不是警告⚠️或者錯誤❌, 程序員們常常會忘記本身標記的這些東西. 經常會發現本身幾個月前標記的問題, 結果拖了幾個月都記不起來. 那麼該怎麼解決呢? 答案就是在run script build phases添加一段編譯腳本.

1.切換到:target-->build phases-->editor-->add run script build phases;
2.選擇:New Run Script Parse;
3.添加以下shell腳本:;

TAGS="TODO:|FIXME:|WARNING:"
ERRORTAG="\/\/ERROR:|\/\/ ERROR:"
find "${SRCROOT}" \( -name "*.h" -or -name "*.m" -or -name "*.swift" \) -and -type f -print0 | xargs -0 egrep --with-filename --line-number --only-matching "($TAGS).*\$|($ERRORTAG).*\$" | perl -p -e "s/($TAGS)/ warning: \$1/"| perl -p -e "s/($ERRORTAG)/ error: \$1/"

Prefect!

11. 一些關於swift的tips

//tip1: we can combine protocols and subtypes with `&`, ex:
protocol A {}
class C {}
class D: C, A {}
func someFunc(using: A & C){}
someFunc(using: D())

//tip2: Protocol extensions can provide default property values
protocol Fadeable {
    var fadeSpeed: TimeInterval {get}
    func fadeOut()
}

extension Fadeable where Self: UIView {
    var fadeSpeed: TimeInterval {return 0.25}
    func fadeOut() {
        UIView.animate(withDuration: fadeSpeed) {
            self.alpha = 0
        }
    }
}
class CustomView: UIView, Fadeable {}

//tip3: Use destructing to manipulate tuple
// 元組能夠當成參數返回值被直接返回; 元祖能夠經過"="直接解構到變量上; 甚至能夠用來交換兩個變量
func getGredentials() -> (String, String) {return ("swift", "python")}
let someStrs = getGredentials()     //直接使用一個變量接收
var (str1, str2) = getGredentials() //使用元祖接收
var (someStr1, someStr2) = someStrs //直接使用兩個變量組成的元組解構另外一個元組變量
(str2, str1) = (str1, str2)         //交換兩個變量的值

//tip4: public getter, private setter
// `public private(set)`, This lets us mark a property as being open for reading but closed for writing
struct Bank {
    public private(set) var address: String
}

//tip5: 定義本身的init可是不覆蓋系統提供的初始化方法
//在結構體中, 若是提供本身的init方法, 則系統再也不提供, 這是爲了防止你在你本身的初始化方法中作了重要的操做, 而
//  這時候若是系統繼續提供初始化方法, 則會忽略你的重要初始化.若是你想要在本身提供初始化方法時候保留系統提供的
//  初始化方法, 則能夠在extension中提供初始化方法.
//在class中, 一樣的道理, 你在extension中提供的init方法不會覆蓋掉系統提供的init方法. 和結構體不一樣的是, 類
//  擴展中的init方法須要使用`convenience`聲明.

//tip6: static和class的區別
// static和class都可以用來聲明類變量和類方法, 他們的區別在於繼承上. class聲明的變量和方法能夠被子類繼承, 而static
//  聲明的不可以被繼承.

//tip7: `==`和`===`的區別
// ==表示比較兩個值是否相等
// ===表示比較兩個變量的內存地址是否相等

12.timer

  1. ios10以後, iOS提供了Timer.scheduledTimer(withTimeInterval: 2, repeats: true, block: <#T##(Timer) -> Void#>)這樣的方法,能夠更加方便的建立和管理Timer.
  2. 使用Timer也能夠完成一些只重複一次的動做, 可是使用GCD的asyncAfter會不會更好一點呢?
  3. 給tiemr附加一些數據. 使用timer的userinfo.
  4. 添加一些tolerance. 給timer添加tolerance能夠減小電量消耗, 這可能致使你的timer執行不精確,好比每秒重複觸發一次的timer, 能夠設置tolerance=0.2s(默認狀況下tolerance=0,可是系統確定會默認添加一個很小的數字), 這樣tiemr最多可能會延遲0.2s, 可是必定不會提早, 如上所述例子, 第一次timer多是1..<1.2s內觸發,第二次可能2..<2.2觸發,第三次可能3..<3.2觸發. 就是說若是第一次延遲, 第二次的觸發時間並不會在第一次的基礎之上延遲. That is all!
  5. 把timer添加到RRunloop中. 默認狀況下timer會被添加到defaultRunLoopMode中, 若是用戶正在滑動一個tabbleView, 那麼你的定時器將不會被觸發. 若是你想在用戶操做UI的同時定時器也在跑, 則你須要將timer添加到commonModes中.
  6. 若是你想讓你的定時器精度很是高, 好比60次/秒, 則此時timer是沒法知足需求的, timer只能用在精度相對比較低的場景, 若是你有經度比較高的需求, 你可使用CADisplayLink.
//使用timer的userinfo. 給timer附加一些數據, Dict類型的數據, 能夠放在參數context中, 以下所示:
let context = ["user": "@twostraws"]
Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(fireTimer), userInfo: context, repeats: true)
//在timer對象中能夠取出timer.userinfo,以下所示
@objc func fireTimer(timer: Timer) {
    guard let context = timer.userInfo as? [String: String] else { return }
    let user = context["user", default: "Anonymous"]
    
    print("Timer fired by \(user)!")
    runCount += 1
    
    if runCount == 3 {
        timer.invalidate()
    }
}

//使用commonModes
let context = ["user": "@twostraws"]
let timer = Timer(timeInterval: 1.0, target: self, selector: #selector(fireTimer), userInfo: context, repeats: true)
RunLoop.current.add(timer, forMode: .commonModes)

摘自: https://www.hackingwithswift.com/articles/117/the-ultimate-guide-to-timer

13.UIActivityViewController,自定義分享的activityAction

詳情參考: https://www.hackingwithswift.com/articles/118/uiactivityviewcontroller-by-example
demo參考測試代碼庫.

14.swift4.0以後,App中自定義UIApplication

1. 建立自定義的UIApplication子類MyApplication;
2. 建立main.swift文件;
3. 在自定義的main.swift中指定初始化的MyApplication;
let _ = UIApplicationMain(
    CommandLine.argc,
    UnsafeMutableRawPointer(CommandLine.unsafeArgv)
        .bindMemory(
            to: UnsafeMutablePointer<Int8>.self,
            capacity: Int(CommandLine.argc)
    ),
    NSStringFromClass(WHApplication.self),
    NSStringFromClass(AppDelegate.self)
)
4.刪除原來Appdelegate中的`@UIApplicationMain`註解.  Done.

15. swift中的隱士解包

隱士可選值是指那些不管什麼時候使用它們都會自動強制解包的可選值. 目前來看, 在項目中形成隱士可選值的方式有兩種:

  1. 暫時來講, 咱們在項目中可能會用到Objective-C方法的返回值或者代理參數, 而在oc中沒有一種標記變量是否有值得體系, 默認oc返回值和參數, 咱們直接使用時候都會引起隱士解包, 若是oc返回的是nil值, 則在隱士強解包時候回形成程序崩潰.
  2. 實際上, 在swift中也能夠定義隱士解包的值, 如var str: String!, 這種狀況下, 若是直接使用str, 則會形成程序崩潰. 在實際開發中, 要避免這種寫法.

雖然隱士可選值在行爲上和非可選值同樣, 可是咱們依然能夠對它們使用可選鏈, nil合併, if let和map, 全部的操做都和可選值同樣.

16. swift線程鎖

16.1 synchronized

func synchronized(lock: AnyObject, closure: () -> ()) {
        objc_sync_enter(lock)
        closure()
        objc_sync_exit(lock)
    }

16.2 dispatch_semaphore

使用dispatch_semaphore建立信號, 使用信號量來控制訪問線程的數量.

17. 動態加載字體

/// 判斷字體是否存在
    private static func isFontLoaded(fontName: String) -> Bool {
        let tmpFont = UIFont(name: fontName, size: 10)
        return tmpFont?.fontName == fontName
    }
    
    /// 動態加載字體
    private static func dynamicLoadFont(fontName: String) {
        guard var fontPath = Bundle.main.path(forResource: "UIFoundationKit.bundle", ofType: nil) else { return }
        fontPath = "\(fontPath)/\(fontName.uppercased()).OTF"
        let url = URL(fileURLWithPath: fontPath)
        if
            let fontData = try? Data(contentsOf: url),
            let provider = CGDataProvider(data: fontData as CFData),
            let font = CGFont.init(provider) {
            CTFontManagerRegisterGraphicsFont(font, nil)
        }
    }

18. swift中的@convention

@convention是用來修飾閉包的, 用來代表此閉包能夠兼容什麼語言格式的閉包. 好比convention(c)表示此閉包能夠傳遞到c函數中. 以下所示:

  1. @convention(swift) : 代表這個是一個swift的閉包;
  2. @convention(block) :代表這個是一個兼容oc的block的閉包;
  3. @convention(c) : 代表這個是兼容c的函數指針的閉包。

使用示例:

let saySomething_c : @convention(c) (String)->Void = {
    print("i said: \($0)")
}

let saySomething_oc : @convention(block) (String)->Void = {
    print("i said: \($0)")
}

let saySomething_swift : @convention(swift) (String)->Void = {
    print("i said: \($0)")
}

19.swift獲取對象內存地址

let obj = Obj()
/// 方案一: 測試中 發現做用在<引用類型>的對象上能確保正確性
let point = Unmanaged<AnyObject>.passUnretained(obj as AnyObject).toOpaque()
let hashValue = point.hashValue // 這個就是惟一的,能夠做比較

/// 方案二:測試中 發現做用在<值類型>的對象上能確保正確性
let hashValue2 = withUnsafePointer(to: &obj) { (point) -> Int in
        /// 閉包的實現有多種,可根據本身需求修改 
        return point.hashValue
 }
相關文章
相關標籤/搜索