Skip to content

Instantly share code, notes, and snippets.

@tx-TEM
Last active September 22, 2022 07:07
Show Gist options
  • Save tx-TEM/39e7dd74473b95543d8633d3f0f919db to your computer and use it in GitHub Desktop.
Save tx-TEM/39e7dd74473b95543d8633d3f0f919db to your computer and use it in GitHub Desktop.
Swift 個人用コードまとめ

コードから利用できるUIViewを作成する

import UIKit

final class CommonSectionHeader: UIView {
    @IBOutlet var titleLabel: UILabel!
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        commonInit()
    }

    func commonInit() {
        let bundle = Bundle(for: type(of: self))
        let nib = UINib(nibName: "\(CommonSectionHeader.self)", bundle: bundle)
        guard let view = nib.instantiate(withOwner: self, options: nil).first as? UIView else {
            fatalError()
        }

        addSubview(view)
        view.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            view.topAnchor.constraint(equalTo: topAnchor),
            view.leadingAnchor.constraint(equalTo: leadingAnchor),
            view.trailingAnchor.constraint(equalTo: trailingAnchor),
            view.bottomAnchor.constraint(equalTo: bottomAnchor),
        ])
    }
}

File's Ownerのカスタムクラスに指定

https://tech-tokyobay.manju.tokyo/archives/163

横幅固定のlableから高さを計算する

guard let text = textLabel.text, let font = textLabel.font else { return }

let labelHeight = text.boundingRect(with: CGSize(width: UIScreen.main.bounds.width,
                                                         height: CGFloat.greatestFiniteMagnitude),
                                            options: [.usesLineFragmentOrigin, .usesFontLeading],
                                            attributes: [.font: font],
                                            context: nil).height

文字列の長さを取得する

let text = "表示幅を知りたい文字列"
let font = UIFont(name: "Hiragino Kaku Gothic ProN", size: 18)
let width = text.size(withAttributes: [NSAttributedString.Key.font : font])

enumのケース一覧を取得する

enum Fruits: CaseIterable {
    case apple, orange, banana
}

Fruits.allCases.count // => 3
Fruits.allCases // => [Fruits.apple, Fruits.orange, Fruits.banana]

http://blog.penginmura.tech/entry/2018/04/14/202031

enum Fruits: String, CaseIterable {
    case apple = "りんご"
    case orange = "みかん"
    case banana = "バナナ"
}

let list = Fruits.allCases.map { $0.rawValue } // => ["りんご"、"みかん"、"バナナ"]

disclosureの下までView(セパレーター)を配置する

// stroryBoardから設定すると上手くいかないのでセルに対して直接制約はる
import UI
import UIKit

class OtherSettingCell: UITableViewCell {
    override func awakeFromNib() {
        super.awakeFromNib()
        commonInit()
    }

    func commonInit() {
        let lineView = UIView()
        lineView.backgroundColor = UIColor.black
        lineView.translatesAutoresizingMaskIntoConstraints = false
        addSubview(lineView)

        NSLayoutConstraint.activate([
            lineView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
            lineView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
            lineView.bottomAnchor.constraint(equalTo: bottomAnchor),
            lineView.heightAnchor.constraint(equalToConstant: 1)
        ])
    }
}

セルにアクセサリーを配置

func bind(accessory: UITableViewCell.AccessoryType = .none) {
    accessoryType = accessory
    
    // アクセサリーの色を変更
    tintColor = .bind
}

UIの見た目を一括で変更

UISwitch.appearance().onTintColor = .red
UITextField.appearance().tintColor = .red
UITextView.appearance().tintColor = .red

画像の比率を保ったまま、Viewいっぱいに表示する

storyBoard: UIImageViewのContentModeをAspectFitに設定。 ImageViewの制約を左右 + 上か下のどちらか片方につけ、高さ指定なし。画像にあったAspectRatio制約をつける

tableViewCellのタップ時にcell内の指定のViewのみハイライトさせる

cell.selectionStyle = .none


func tableView(_ tableView: UITableView, didHighlightRowAt indexPath: IndexPath) {
    let cell = tableView.cellForRow(at: indexPath) as? BannerCell
    cell?.bannerImageView.backgroundColor = .darkGray
}

func tableView(_ tableView: UITableView, didUnhighlightRowAt indexPath: IndexPath) {
    let cell = tableView.cellForRow(at: indexPath) as? BannerCell
    cell?.bannerImageView.backgroundColor = .clear
}

switchでアンラップ

switch (detail.payment, detail.income) {
case let (payment?, income?):
    print(payment)
    print(income)
case let (payment?, _):
    print(payment)
case let (_ , income?):
    print(income)
default:
    assertionFailure()
}

参考: https://teratail.com/questions/158192#reply-237348

条件に?が付いているとnil検査が先に行われ、nilでない場合に条件が検査されます。

subviewsをprint

func recursive(views: [UIView], count: Int) {
    views.forEach {
        for _ in 0..<count {
            print("  ", terminator: "")
        }
        print("▼[\($0)]")
        if $0.subviews.isNotEmpty {
            recursive(views: $0.subviews, count: count + 1)
        }
    }
}

UILabelの一部の文字列の色を変更

let label = UILabel()
let attrText = NSMutableAttributedString()
let colorAttributes = [NSAttributedString.Key.foregroundColor: UserColorManager().currentTheme.stdColor]

attrText.append(NSMutableAttributedString(string: "色なし "))
attrText.append(NSMutableAttributedString(string: "色ある", attributes: colorAttributes))
label.attributedText = attrText

ReactiveSwiftでVCのライフサイクルイベントのタイミングで処理を動かす

extension Reactive where Base: UIViewController {
    public var viewDidDisappear: Signal<(), Never> {
        return trigger(for: #selector(UIViewController.viewDidDisappear(_:)))
    }

    public var viewWillDisappear: Signal<(), Never> {
        return trigger(for: #selector(UIViewController.viewWillDisappear(_:)))
    }

    public var viewDidAppear: Signal<(), Never> {
        return trigger(for: #selector(UIViewController.viewDidAppear(_:)))
    }

    public var viewWillAppear: Signal<(), Never> {
        return trigger(for: #selector(UIViewController.viewWillAppear(_:)))
    }
}

signal
    .take(during: reactive.lifetime)
    .sample(on: reactive.viewDidAppear)
    .observeValues { [weak self] _ in
        //reloadとか
}

RxSwift: https://tech-blog.sgr-ksmt.org/2016/04/23/viewcontroller_trigger/

ReactiveSwiftでdelegateMethodをsignalProducerに変換

reactive.signal(for: #selector(delegateMethod(_:))).producer.flatMap(.merge) { value -> SignalProducer<XXResponse, Never> in
    guard let response = value[0] as? XXResponse else {
        return SignalProducer.never
    }
    return SignalProducer(value: response)
}

AttributedTextの属性を一括で設定

let attrText = NSMutableAttributedString()
attrText.append(NSMutableAttributedString(string: "xxx", attributes: attributes1))
attrText.append(NSMutableAttributedString(string: "yyy", attributes: attributes2))


// fontサイズだけ後から指定したい場合など
// (AttributedTextはautoShrinkが効かないことがあるので、自前で修正するときに使える)
let range = NSRange(location: 0, length: attrText.length)
attrText.addAttribute(NSAttributedString.Key.font, value: UIFont.systemFont(ofSize: 10), range: range)

frameサイズにあった最適なFontを返す

func ajustedFont(text: String, width: CGFloat, fontSize: CGFloat, weight: UIFont.Weight) -> UIFont {
    let str = NSString(string: text)
    var fSize = fontSize

    while str.size(withAttributes: [
        .font: UIFont.systemFont(ofSize: fSize, weight: weight),
    ]).width >= width {
        fSize -= 0.5
    }
    return UIFont.systemFont(ofSize: fSize, weight: weight)
}

TableViewのrow, section管理用のenum

extension ExampleViewModel {
    typealias Section = ExampleSection
    typealias RowType = ExampleRow

    enum NotificationSettingSection: CaseIterable {
        case secA, secB

        var rows: [RowType] {
            switch self {
            case .secA:
                return [.rowA]
            case .secB:
                return [.rowB, .rowC]
            }
        }
    }

    enum NotificationSettingRow {
        case rowA, rowB, rowC
    }

    var sections: [Section] {
        return Section.allCases
    }

    func sectionFor(section: Int) -> Section? {
        guard sections.indices.contains(section) else { return nil }
        return sections[section]
    }

    func rowFor(indexPath: IndexPath) -> RowType? {
        guard let section = sectionFor(section: indexPath.section), section.rows.indices.contains(indexPath.row) else {
            return nil
        }
        return section.rows[indexPath.row]
    }
}

OptionalのflatMapを活用しコードをシンプルにする

参考: https://scior.hatenablog.com/entry/2020/03/02/230404

let urlString: String? = "url"

// flatMapを使うことでurlStringのunwrapを無くせる
let url: URL? = urlString.flatMap(URL.init(string:))

mapを使って配列を任意のオブジェクトに変換する(mapにクロージャーを渡さない)

struct Data {
  var str: String
  var num: Int
  
  init(num: Int) {
    str = String(num)
  }
}

[1, 2, 3, 4, 5].map(Data.init(num:))

http://swiftlife.hatenablog.jp/entry/2015/12/27/213058

UITableViewCell間に余白を追加する

参考: https://stackoverflow.com/questions/6216839/how-to-add-spacing-between-uitableviewcell

contentView配下に任意のサイズのView(以後 InnerView)を追加し、タップ範囲をInnerViewに限定する

class ExampleCell: UITableViewCell {
    @IBOutlet var innerView: UIView!

    // innerView配下のviewのみをタッチ対象とする
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        let view = super.hitTest(point, with: event)
        
        // 余白部分が対象になった場合は無視
        if view == contentView {
            return nil
        }
        return view
    }
    
    override func setSelected(_ selected: Bool, animated: Bool) {
        highlightBackgroundColor(selected, animated: animated)
    }

    override func setHighlighted(_ highlighted: Bool, animated: Bool) {
        highlightBackgroundColor(highlighted, animated: animated)
    }

    func highlightBackgroundColor(_ highlighted: Bool, animated: Bool) {
        if animated {
            UIView.animate(withDuration: 0.5) {
                self.highlightBackgroundColor(highlighted, animated: false)
            }
        } else {
            innerView.backgroundColor = highlighted ? .lightGray : .white
        }
    }
}

switchでの条件分岐に whereを使う

enum Fruits {
    case grape
    case apple
    case orange
}

let fruits = Fruits.orange
let isWinter = true

switch fruits {
case .grape:
    print("好き")
case .orange where isWinter:
    print("冬は好き")
case .apple, .orange:
    print("あまり好きじゃない")
}

(case let(xx)系にしか使えないと思っていました

SwiftUI Stackを角丸にする

struct BarChart: View {
    var rate: Double

    var body: some View {
        GeometryReader { geometry in
            HStack(alignment: .center, spacing: 0) {
                Color(UIColor.red)
                    .frame(width: geometry.size.width * CGFloat(rate),
                           height: geometry.size.height, alignment: .center)
                Color(UIColor.gray)
                    .frame(width: 1 - (geometry.size.width * CGFloat(rate)),
                           height: geometry.size.height, alignment: .center)
            }
            .mask(RoundedRectangle(cornerRadius: 4))
        }
    }
}

全ての要素のサイズを指定しないと、上手く描画されない模様。

DarkModeへの変更を検知して、なんらかの処理を実行する

final class someView: UIView {
    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)
        // 何らかの処理
    }
}

tableHeaderViewをAutoLayoutに対応させる

https://qiita.com/fromkk/items/de4cbebb3c39ac3888cc

https://twitter.com/k_katsumi/status/1255456988944912387?s=20

headerView.translatesAutoresizingMaskIntoConstraints = false
tableView.tableHeaderView = headerView
NSLayoutConstraint.activate([
    headerView.topAnchor.constraint(equalTo: tableView.topAnchor),
    headerView.widthAnchor.constraint(equalTo: tableView.widthAnchor),
    headerView.centerXAnchor.constraint(equalTo: tableView.centerXAnchor)])
headerView.layoutIfNeeded()
tableView.tableHeaderView = headerView

メモ: tabelHeaderViewの高さが低くなったときに、headerとセルの間に余白ができてしまうことがある。そういう場合はtableView.reloadDataすると直る

特定のプロトコルに準拠した型を引数に持つメソッドを定義する

public protocol HalfModalContent {
}

class HalfModalViewController {
  static func instantiate<T: UIViewController>(contentVC: T) -> HalfModalViewController where T: HalfModalContent  {} 
}

UIPresentationControllerでVCのprefferdContentSizeの変更に合わせてレイアウトを変更する

override public func preferredContentSizeDidChange(forChildContentContainer _: UIContentContainer) {
    guard let containerView = containerView else {
        return
    }
    
    UIView.animate(withDuration: 0.3) {
        containerView.setNeedsLayout()
        containerView.layoutIfNeeded()
    }
}

override public func containerViewWillLayoutSubviews() {
    super.containerViewWillLayoutSubviews()
        presentedView!.frame = frameOfPresentedViewInContainerView
}

横幅に制限のあるViewをうまいこと中心に配置する

NSLayoutConstraint.activate([
    imageView.leadingAnchor.constraint(greaterThanOrEqualTo: containerView.leadingAnchor),
    imageView.trailingAnchor.constraint(lessThanOrEqualTo: containerView.trailingAnchor),
    imageView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
    imageView.widthAnchor.constraint(lessThanOrEqualToConstant: 700),
])

viewがはみ出ないか心配だったので左右の余白をつけたが、もしかしたらいらないかもしれない。

dateの加算

Calendar.current.date(byAdding: .hour, value: -1, to: Date())

StackView内のCollectionViewの高さが0にならないようにする

https://zenn.dev/kalupas226/articles/da4b25f96c8c2d

@IBOutlet private weak var collectionView: UICollectionView!

//...

override func viewDidload() {
  super.viewDidLoad()

  let layout = UICollectionViewFlowLayout()
  layout.scrollDirection = .horizontal
  layout.estimatedItemSize = CGSize(width: 1, height: 1)
  layout.invalidateLayout()
  collectionView.collectionViewLayout = layout
}

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    collectionViewHeightConstraint.constant = collectionView.collectionViewLayout.collectionViewContentSize.height
    layoutIfNeeded()
}

テーブルビューのセクションヘッダから余分なpaddingを無くす

if #available(iOS 15.0, *) {
    UITableView.appearance().sectionHeaderTopPadding = 0
}

UITableViewの任意のセルだけ高さを可変にする

func tableView(_: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    guard let currentSection = viewModel.sectionFor(section: indexPath.section) else {
        return 0
    }
    switch currentSection {
    case .item:
        return 100
    case .footer:
        return UITableView.automaticDimension
    }
}

Compositional Layoutのテンプレ

final class CompositionalLayoutViewController: UIViewController {
    typealias ViewModel = CompositionalLayoutViewModel
    typealias Section = ViewModel.Section
    typealias Item = ViewModel.Item

    @IBOutlet var collectionView: UICollectionView!
    let viewModel = CompositionalLayoutViewModel()

    private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
    private var snapshot: NSDiffableDataSourceSnapshot<Section, Item>!

    override func viewDidLoad() {
        super.viewDidLoad()
        setupCollectionView()
    }
}

extension CompositionalLayoutViewController {
    private func compositionalLayout() -> UICollectionViewCompositionalLayout {
        let layout = UICollectionViewCompositionalLayout { [weak self] _, _ in
            guard let self = self else { fatalError() }
            return self.createSection()
        }
        return layout
    }

    private func createSection() -> NSCollectionLayoutSection {
        let itemWidth: CGFloat = 85
        let itemCount = 12

        let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(itemWidth), heightDimension: .absolute(44))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)
        let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(CGFloat(itemCount) * itemWidth), heightDimension: .absolute(44))
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 12)
        let section = NSCollectionLayoutSection(group: group)
        return section
    }

    private func setupCollectionView() {
        collectionView.register(SampleItemCell.self, forCellWithReuseIdentifier: "\(SampleItemCell.self)")

        collectionView.bounces = false
        collectionView.collectionViewLayout = compositionalLayout()
        dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, item in
            switch item {
            case let .sampleItem(data):
                let cell = collectionView.dequeueCell("\(SampleItemCell.self)", for: indexPath) as SampleItemCell
                cell.setup(parentController: self, title: "\(data.value)")
                return cell
            }
        }

        snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
        snapshot.appendSections(Section.allCases)

        for i in 1 ... 12 {
            snapshot.appendItems([.columnsItem(ViewModel.SampleData(value: i))], toSection: .sample)
        }
        dataSource.apply(snapshot, animatingDifferences: false)
    }
}

struct CompositionalLayoutViewModel {}
extension CompositionalLayoutViewModel {
    enum Section: CaseIterable {
        case sample
    }

    enum Item: Hashable {
        case sampleItem(SampleData)
    }

    struct SampleData: Hashable {
        private let identifier = UUID()
        var value: Int
    }
}

ContainerViewをコードで作る

vc.willMove(toParent: self)
addChild(vc)
vc.didMove(toParent: self)
vc.view.translatesAutoresizingMaskIntoConstraints = false

let contentView = UIView()
contentView.addSubview(vc.view)
NSLayoutConstraint.activate([
    vc.view.topAnchor.constraint(equalTo: contentView.topAnchor),
    vc.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
    vc.view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
    vc.view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
])

Compositional LayoutでヘッダーViewの表示

func createHeaderRegistration() -> UICollectionView.SupplementaryRegistration<HeaderView> {
    return UICollectionView.SupplementaryRegistration<HeaderView>(
        supplementaryNib: UINib(nibName: "\(OnlineAccountTutorialListHeaderView.self)", bundle: nil),
        elementKind: UICollectionView.elementKindSectionHeader) { (supplementaryView, string, indexPath) in
            supplementaryView.setup(title: title)
        }
}

let headerRegistration = createHeaderRegistration()
dataSource.supplementaryViewProvider = { (view, kind, index) in
    return self.collectionView.dequeueConfiguredReusableSupplementary(using: headerRegistration, for: index)
}

UIViewControllerへのDI

    // DI用のinitializer
    init?(coder: NSCoder, viewModel: ViewModel) {
        self.viewModel = viewModel
        super.init(coder: coder)
    }

    required init?(coder: NSCoder) {
        fatalError()
    }

ListスタイルのUICollectionViewのセパレーターを非表示にする

var configuration = UICollectionLayoutListConfiguration(appearance: .plain)
configuration.showsSeparators = false

ボタンのタイトル変更時にアニメーションしないようにする

理由は分かりませんが、たまに謎にアニメーションしてしまうことがある

UIView.performWithoutAnimation {
    button.setTitle(configuration.title, for: .normal)
    button.layoutIfNeeded()
}

ListスタイルのCompositionnal Layout でセクション間に余白を入れる

let layout = UICollectionViewCompositionalLayout { sectionIndex, layoutEnvironment in
    let section = NSCollectionLayoutSection.list(using: configuration, layoutEnvironment: layoutEnvironment)
    if sectionIndex == 0 {
        section.contentInsets = NSDirectionalEdgeInsets(top: 24, leading: 0, bottom: 0, trailing: 0)
    }
    return section
}

collectionViewにSwiftUIで作成したViewを表示する

final class HostingContentViewConfiguration<T: View>: UIContentConfiguration {
    private(set) weak var parentVC: UIViewController?
    private(set) var content: () -> T

    init(parentVC: UIViewController?, @ViewBuilder content: @escaping () -> T) {
        self.parentVC = parentVC
        self.content = content
    }

    func makeContentView() -> UIView & UIContentView {
        return HostingContentView(configuration: self)
    }

    func updated(for _: UIConfigurationState) -> Self {
        return self
    }
}

final class HostingContentView<T: View>: UIView, UIContentView {
    var hostingController: UIHostingController<T>

    var configuration: UIContentConfiguration {
        didSet {
            guard let config = configuration as? HostingContentViewConfiguration else {
                return
            }
            removeHostingControllerFromParent()
            hostingController = UIHostingController(rootView: config.content())
            setup(parentVC: config.parentVC)
        }
    }

    init(configuration: HostingContentViewConfiguration) {
        self.configuration = configuration
        hostingController = UIHostingController(rootView: configuration.content())
        super.init(frame: .zero)
        setup(parentVC: configuration.parentVC)
    }

    @available(*, unavailable)
    required init?(coder _: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    deinit {
        removeHostingControllerFromParent()
    }
}

extension HostingContentView {
    public func setup(parentVC: UIViewController?) {
        guard let parentVC = parentVC else {
            return
        }
        let vc = hostingController
        vc.view.backgroundColor = .clear
        vc.willMove(toParent: parentVC)
        parentVC.addChild(vc)
        vc.didMove(toParent: parentVC)
        vc.view.translatesAutoresizingMaskIntoConstraints = false
        addSubview(vc.view)
        NSLayoutConstraint.activate([
            vc.view.topAnchor.constraint(equalTo: topAnchor),
            vc.view.bottomAnchor.constraint(equalTo: bottomAnchor),
            vc.view.leadingAnchor.constraint(equalTo: leadingAnchor),
            vc.view.trailingAnchor.constraint(equalTo: trailingAnchor),
        ])
    }

    public func removeHostingControllerFromParent() {
        hostingController.willMove(toParent: nil)
        hostingController.view.removeFromSuperview()
        hostingController.removeFromParent()
    }
}


private func cellRegistration() -> UICollectionView.CellRegistration<UICollectionViewCell, Item> {
    return UICollectionView.CellRegistration<UICollectionViewCell, Item> { [weak self] cell, _, _ in
        cell.contentConfiguration = HostingContentViewConfiguration(parentVC: self) {
            ContentView()
        }
    }
}

メモ

作成したViewへのBindingにはObservableObjectを直接渡す。vcでobservableObjectを持ち、ViewにPublished指定したプロパティを渡しても更新は入らない

UICollectionViewDiffableDataSourceでRealmのオブジェクトを管理する

自分の環境によるかもしれませんが、Resultsを直接扱うと、オブジェクトを削除したときに、「"Object has been deleted or invalidated."」というクラッシュが発生しました。 対策として、results.freeze()のような形にして、オブジェクトの中身が自動で更新されないようにすることで解消しました。

削除処理の例

let realm = try! Realm()
try! realm.write {
    let objects = XXObjectsManager.shared.all()
    realm.delete(objects)
}

対応

var objects = XXObjectsManager.shared.all().freeze()

何らかの方法でrealmのobject変更を監視 {
  objects = XXObjectsManager.shared.all().freeze()
  // viewの更新処理
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment