UILabelのattributedTextのテキストを他の属性は変更せずに別の文字列に置き換える

環境: Swift5.0

UILabelのattributedTextのテキストを他の属性は変更せずに別の文字列に置き換える例。

@IBOutlet weak var textLabel: UILabel!

private func setupUI() {
    if let attrStr = textLabel.attributedText?.mutableCopy() as? NSMutableAttributedString {
        attrStr.mutableString.setString("置き換える文字列")
        textLabel.attributedText = attrStr
    }
}

ちなみに文字列の一部分を置き換える場合は以下で書いた。

xyk.hatenablog.com

MacのDockアイコンがおかしいときにやったこと

MacのDock上に表示されるアプリのアイコンがデフォルトと言うか generic なアイコンになってしまった。
元に戻すためにやった手順をメモ。
先に結論を書いておくとアイコンのキャッシュを削除することで元に戻った。

まずSIP(System Integrity Protection)を無効(disable)にしておく必要がある。
Command + R を押しながらMac起動し、リカバリーモードで起動する。
リカバリーモードが起動したら、ユーティリティからターミナルを起動し

$ csrutil status

で現在の状態を確認、enabled であれば

$ csrutil disable

を実行する。
その後、Macを再起動。
そして、以下コマンドでキャッシュ削除を行う。

$ sudo find /private/var/folders/ -name 'com.apple.dock.iconcache' -delete
$ sudo find /private/var/folders/ -name 'com.apple.iconservices' -delete
$ sudo rm -r /Library/Caches/com.apple.iconservices.store
$ killall Dock

UIColorの色を暗くする

Swift 4.2.1

現在の色から暗めの色に明度(brightness/lightness)を変更するための UIColor Extension。

extension UIColor {
    
    func dark(brightnessRatio: CGFloat = 0.8) -> UIColor {
        var hue: CGFloat = 0
        var saturation: CGFloat = 0
        var brightness: CGFloat = 0
        var alpha: CGFloat = 0
        
        if getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) {
            return UIColor(hue: hue, saturation: saturation, brightness: brightness * brightnessRatio, alpha: alpha)
        } else {
            return self
        }
    }
}

使い方

view.backgroundColor = view.backgroundColor.dark()

複数の CLLocationManager を使う

Swift 4.2.1
Deployment Target: 9.0

CoreLocation で位置情報を取得する際に、複数のCLLocationManagerインスタンスを同時に立ち上げて動かしたときにどうなるか気になったので試してみた。
結論としては、それぞれのインスタンスが影響し合うことなく別々に制御(startやstop) できた。

以下検証コード。

import UIKit
import CoreLocation

class ViewController: UIViewController {

    private let locationServiceA = LocationService.init(tag: "A")
    private let locationServiceB = LocationService.init(tag: "B")
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    @IBAction func handleStartButtonA(_ sender: UIButton) {
        locationServiceA.startUpdating()
    }
    @IBAction func handleStartButtonB(_ sender: UIButton) {
        locationServiceB.startUpdating()
    }
    @IBAction func handleStopButtonA(_ sender: UIButton) {
        locationServiceA.stopUpdating()
    }
    @IBAction func handleStopButtonB(_ sender: UIButton) {
        locationServiceB.stopUpdating()
    }
}

class LocationService: NSObject {
    private let locationManager = CLLocationManager()
    private var tag = ""
    
    override init() {
        super.init()
        locationManager.delegate = self
    }
    
    convenience init(tag: String) {
        self.init()
        self.tag = tag
    }
    
    func startUpdating() {
        print("tag:\(tag) startUpdating")
        locationManager.startUpdatingLocation()
    }
    
    func stopUpdating() {
        print("tag:\(tag) stopUpdating")
        locationManager.stopUpdatingLocation()
    }
    
    func checkPermisson() {
        switch CLLocationManager.authorizationStatus() {
        case .notDetermined:
            print("notDetermined")
        case .restricted:
            print("restricted")
        case .denied:
            print("denied")
        case .authorizedAlways:
            print("authorizedAlways")
        case .authorizedWhenInUse:
            print("authorizedWhenInUse")
        }
    }
}

extension LocationService: CLLocationManagerDelegate {
    
    //  このメソッドは locationManager.delegate = self を実行したタイミングでまず1回呼ばれる
    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        print("tag:\(tag) didChangeAuthorization -> ", status.rawValue)
        if status == .notDetermined {
            locationManager.requestWhenInUseAuthorization()
        }
    }
    
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        if let location = locations.last {
            let latitude = location.coordinate.latitude
            let longitude = location.coordinate.longitude
            let timestamp = location.timestamp.description
            print("tag:\(tag) didUpdateLocations -> latitude:\(latitude) longitude:\(longitude) timestamp:\(timestamp)")
        }
    }
    
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        print("tag:\(tag) didFailWithError -> ", error)
    }
}

Info.plist に以下追加

アプリ使用時のみ位置情報を取得する場合

<key>NSLocationWhenInUseUsageDescription</key>
<string>位置情報を確認するために利用します。</string>

常に位置情報を取得する場合

<key>NSLocationAlwaysUsageDescription</key>
<string>位置情報を確認するために利用します。</string>

UITextField, UITextView がキーボードで隠れないようにする - Swift 版

環境:
Swift 4.2.1
Deployment Target: 11.0

以前にも同じ内容の記事を書いたが久しぶりに Swift でも同じような実装をしたのでメモ。

前提として
- UIViewController.view に UIScrollView を貼り付け
- UIScrollView 上に UITextField, UITextView を貼り付け
- UIScrollView の bottom は view.safeAreaInsets.bottom に合わせる
という条件になっている。

FirstResponder となっている UITextField, UITextView を探す UIView Extension

extension UIView {
    func findFirstResponder() -> UIView? {
        if isFirstResponder {
            return self
        }
        for v in subviews {
            if let responder = v.findFirstResponder() {
                return responder
            }
        }
        return nil
    }
}

Keyboard の Notification.userInfo をマッピングする Struct

struct UIKeyboardInfo {
    let frame: CGRect
    let animationDuration: TimeInterval
    let animationCurve: UIView.AnimationOptions
    
    init?(info: [AnyHashable : Any]) {
        guard
            let frame = (info[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue,
            let duration = info[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval,
            let curve = info[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt
            else { return nil }
        self.frame = frame
        animationDuration = duration
        animationCurve = UIView.AnimationOptions(rawValue: curve)
    }
}

UIScrollView を探す UIView Extension

extension UIView {
    func findSuperView<T>(ofType: T.Type) -> T? {
        if let superView = self.superview {
            switch superView {
            case let superView as T:
                return superView
            default:
                return superView.findSuperView(ofType: ofType)
            }
        }
        return nil
    }
}

ViewController

class MyViewController: UIViewController {
    
    //@IBOutlet weak var scrollView: UIScrollView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        NotificationCenter.default.addObserver(self, selector: #selector(onKeyboardWillShow(_:)),
                                               name: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(onKeyboardWillHide(_:)),
                                               name: UIResponder.keyboardWillHideNotification, object: nil)
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
        
        super.viewWillDisappear(animated)
    }
    
    @objc private func onKeyboardWillShow(_ notification: Notification) {
        guard
            let userInfo = notification.userInfo,
            let keyboardInfo = UIKeyboardInfo(info: userInfo),
            let inputView = view.findFirstResponder(),
            let scrollView = inputView.findSuperView(ofType: UIScrollView.self)
            else { return }
        
        let inputRect = inputView.convert(inputView.bounds, to: scrollView)
        let keyboardRect = scrollView.convert(keyboardInfo.frame, from: nil)
        let offsetY = inputRect.maxY - keyboardRect.minY
        if offsetY > 0 {
            let contentOffset = CGPoint(x: scrollView.contentOffset.x, y: scrollView.contentOffset.y + offsetY)
            scrollView.contentOffset = contentOffset
        }
        // 例えば iPhoneX の Portrait 表示だと bottom に34ptほど隙間ができるのでその分を差し引く  
        let contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardInfo.frame.height - view.safeAreaInsets.bottom, right: 0)
        scrollView.contentInset = contentInset
        scrollView.scrollIndicatorInsets = contentInset
    }
    
    @objc private func onKeyboardWillHide(_ notification: Notification) {
        guard
            let inputView = view.findFirstResponder(),
            let scrollView = inputView.findSuperView(ofType: UIScrollView.self)
            else { return }
        scrollView.contentInset = .zero
        scrollView.scrollIndicatorInsets = .zero
    }
}

ある UIColor から別の UIColor に徐々に色を変化させる

環境: Swift 4.2.1

ある UIColor から別の UIColor に徐々に色を変化させるヘルパーメソッド。

extension UIColor {

    static func colorLerp(from: UIColor, to: UIColor, progress: CGFloat) -> UIColor {
        
        let t = max(0, min(1, progress))
        
        var redA: CGFloat = 0
        var greenA: CGFloat = 0
        var blueA: CGFloat = 0
        var alphaA: CGFloat = 0
        from.getRed(&redA, green: &greenA, blue: &blueA, alpha: &alphaA)
        
        var redB: CGFloat = 0
        var greenB: CGFloat = 0
        var blueB: CGFloat = 0
        var alphaB: CGFloat = 0
        to.getRed(&redB, green: &greenB, blue: &blueB, alpha: &alphaB)
        
        let lerp = { (a: CGFloat, b: CGFloat, t: CGFloat) -> CGFloat in
            return a + (b - a) * t
        }
        
        let r = lerp(redA, redB, t)
        let g = lerp(greenA, greenB, t)
        let b = lerp(blueA, blueB, t)
        let a = lerp(alphaA, alphaB, t)
        
        return UIColor(red: r, green: g, blue: b, alpha: a)
    }
}

progress を徐々に変化させることで色が変化していく。

let color = UIColor.colorLerp(from: .white, to: .black, progress: 0.3)