カテゴリー
iOS Swift

UserDefaultsにデータを保存する

// to set data in UserDefaults
UserDefaults.standard.set(Float(0.0), forKey: "some_key")

// to fetch from UserDefaults
UserDefaults.standard.float(forKey: "some_key")
カテゴリー
iOS Swift

UINavigationControllerサンプルコード

globalにNavigationControllerを設定するにはSceneDelegate内で設定をします。

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
    // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
    // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
    guard let windowScene = (scene as? UIWindowScene) else { return }
    window = UIWindow(frame: windowScene.coordinateSpace.bounds)
    window?.windowScene = windowScene
    let navigationController = UINavigationController(rootViewController: SequenceViewController())
    window?.rootViewController = navigationController
    window?.makeKeyAndVisible()
}

別のスクリーンへ遷移するには以下のようにします。

@objc private func goToVC() {
  let targetVC = NextViewController()
  navigationController?.pushViewController(targetVC, animated: true)
}

iOSのスクリーン遷移は、navigationController.viewControllersという配列にUIViewControllerをスタックする形で管理します。試しに、遷移先のviewDidLoad内でviewControllers配列をprintしてみます。その時点で表示されているViewControllerは配列内の一番最後のものだと分かります。

// print(navigationController?.viewControllers)

Optional([<ChordGenius.ViewController: 0x106817200>, <ChordGenius.ChordSelectVC: 0x105f07bd0>])

ひとつ前のViewControllerに戻るには、popViewController()を使います。なお、popToRootViewController()は一番最初のVCに戻る時に使います。

Transitionの挙動を設定するにはCATransitionを使い、navigationControllerに設定します。

@objc private func transitionButtonTapped() {
    let transition = CATransition()
    transition.duration = 0.5
    transition.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
    transition.type = .fade
    navigationController?.view.layer.add(transition, forKey: nil)
    navigationController?.popToRootViewController(animated: false)
}

個別のviewController内に新しいnavigationControllerインスタンスを作成し、presentすることも出来ます。

@objc private func transitionButtonTapped() {
  let targetVC = BookmarkedViewController()
  let navigationVC = UINavigationController(rootViewController: targetVC)
  navigationVC.modalTransitionStyle = .crossDissolve
  navigationVC.modalPresentationStyle = .fullScreen
  self.present(navigationVC, animated: true)
}

viewDidLoad内でNavigationItemを設定するには以下のようにします。

// some basic examples
title = "Manage Songs"
navigationController?.navigationBar.prefersLargeTitles = true
    self.navigationItem.setRightBarButton(UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addTapped)), animated: true)

余談ですが、UINavigationControllerを設定するとview.safeAreaInsets.topの値が少し大きくなります。

カテゴリー
iOS Swift

SVGをUIButtonのアイコンとして使う

SVGをiOSで使う場合はまずAssetsフォルダにSVGを保存し、以下のように設定します。

こうすることで通常のSF Symbolのアイコンのように tintColorが反映されるようになります。

lazy var gotoSequenceButton: UIButton = {
    let button = UIButton()
    button.setImage(UIImage(named: "play.square.stack"), for: .normal)
    button.imageView?.contentMode = .scaleAspectFit
    button.contentHorizontalAlignment = .fill
    button.contentVerticalAlignment = .fill
    button.tintColor = .white
    button.addTarget(self, action: #selector(onGotoSequenceButtonTapped), for: .touchUpInside)
    return button
}()
カテゴリー
iOS Swift

ViewControllerごとにOrientationを指定したい

iOS15 参考

iOS16以降 参考

Helper

class Helper {
  static func forceRotate(orientation: UIInterfaceOrientationMask) {
    (UIApplication.shared.delegate as? AppDelegate)?.orientation = orientation
  let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene

  windowScene?.requestGeometryUpdate(.iOS(interfaceOrientations: orientation))

UIApplication.navigationTopViewController()?.setNeedsUpdateOfSupportedInterfaceOrientations()
  }
}

extension UIApplication {
  class func navigationTopViewController() -> UIViewController? {
    let nav = UIApplication.shared.keyWindow?.rootViewController as? UINavigationController
    return nav?.topViewController
  }
}

appDelegate

var orientation: UIInterfaceOrientationMask = .portrait
  
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
    return orientation
}

viewDidLoad()

override func viewDidLoad() {
  super.viewDidLoad()

  Helper.forceRotate(orientation: .portrait)
}
カテゴリー
iOS Swift

Custom Slider Barを作ってみた

Github Repo

UIViewのoverride func touchesMoved() のtouchesは色々な情報を含んでいますが、その中のtouches: Set<UITouch>を利用しています。

ポイントとしては。。。

scaleBar.layer.anchorPointのイニシャル値はセンター(0.5, 0.5)ですが、(0.5, 1)とすることでshapeの底辺中央をanchorPointに設定できます。

iOSではy値はその自身の左上コーナーを基点とするので、自身のheightからy値を引いたものをyValueとし、scaleBy(x: 1, yValue)でy方向にのみscaleさせています。

0 <= yValue <= height としたいため、max(min(height, touch.location(in: self).y), 0) としています。

RxSwiftのBehaviorRelayを使って、スライダーの値が変化する度にUIなどを変更出来るようにしています。

lazy var scaleBar: UIView = {
    let scaleBar = UIView()
    scaleBar.translatesAutoresizingMaskIntoConstraints = false
    scaleBar.backgroundColor = .blue
    // initial anchorPoint is (0.5, 0.5) and the shape stretches from the center
    scaleBar.layer.anchorPoint = CGPoint(x: 0.5, y: 1)
    return scaleBar
}()

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard let touch = touches.first else { return }
    let yValue = height - max(min(height, touch.location(in: self).y), 0)
    yValueRelay.accept(yValue)
    scaleBar.transform = startTransform?.scaledBy(x: 1, y: yValue) ?? CGAffineTransform.identity
}
カテゴリー
iOS Swift

UITableViewを使ってカスタムPickerViewのようなものを作る

Sample Code

class ViewController: UIViewController {

  let dataCount = 10 // this will be replaced by the datasource count.
  
  var currentIndex: Int = 0 {
    didSet {
      print("aboveButton.isHidden: \(aboveButton.isHidden)")

      if currentIndex == 0 {
        UIView.animate(withDuration: 1, animations: {
          self.aboveButton.alpha = 0
        })
      } else if currentIndex == dataCount - 1 {
        UIView.animate(withDuration: 1, animations: {
          self.belowButton.alpha = 0
        })
      }
      else {
        UIView.animate(withDuration: 0.5, animations: {
          self.aboveButton.alpha = 1
          self.belowButton.alpha = 1
        })
      }
    }
  }
  
  let tableViewHeight: CGFloat = 300
  
  let buttonSize: CGFloat = 50
  
  lazy var aboveButton: UIButton = {
    let button = UIButton()
    button.setImage(UIImage(systemName: "chevron.up"), for: .normal)
    button.imageView?.contentMode = .scaleAspectFit
    button.contentHorizontalAlignment = .fill
    button.contentVerticalAlignment = .fill
    button.tintColor = .systemGray
    button.addTarget(self, action: #selector(onAboveButtonTapped), for: .touchUpInside)
    return button
  }()
  
  @objc private func onAboveButtonTapped() {
    let index = currentIndex - 1
    guard index >= 0 else { return }
    let indexPath = IndexPath(row: index, section: 0)
    tableView.scrollToRow(at: indexPath, at: .top, animated: true)
    currentIndex = index
  }
  
  lazy var belowButton: UIButton = {
    let button = UIButton()
    button.setImage(UIImage(systemName: "chevron.down"), for: .normal)
    button.imageView?.contentMode = .scaleAspectFit
    button.contentHorizontalAlignment = .fill
    button.contentVerticalAlignment = .fill
    button.tintColor = .systemGray
    button.addTarget(self, action: #selector(onBelowButtonTapped), for: .touchUpInside)
    return button
  }()
  
  @objc private func onBelowButtonTapped() {
    let index = currentIndex + 1
    guard index <= dataCount - 1 else { return }
    let indexPath = IndexPath(row: index, section: 0)
    tableView.scrollToRow(at: indexPath, at: .top, animated: true)
    currentIndex = index
  }
  
  lazy var tableView: UITableView = {
    let tableView = UITableView()
    tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
    tableView.separatorColor = .clear
    tableView.rowHeight = tableViewHeight
    return tableView
  }()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    view.backgroundColor = .white
    view.addSubview(tableView)
    view.addSubview(aboveButton)
    view.addSubview(belowButton)
    if currentIndex == 0 {
      aboveButton.alpha = 0
    }
    if currentIndex == dataCount - 1 {
      belowButton.alpha = 0
    }
    tableView.delegate = self
    tableView.dataSource = self
  }
  
  override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()
    layout()
    
  }
  
  private func layout() {
    tableView.frame = CGRect(x: 0, y: view.bounds.height/2 - tableViewHeight/2, width: view.bounds.width, height: tableViewHeight)
    aboveButton.frame = CGRect(x: view.bounds.width/2 - buttonSize/2, y: tableView.frame.minY + 10, width: buttonSize, height: buttonSize)
    belowButton.frame = CGRect(x: view.bounds.width/2 - buttonSize/2, y: tableView.frame.minY + tableViewHeight - 10 - buttonSize, width: buttonSize, height: buttonSize)
  }

}

extension ViewController: UITableViewDelegate {
  
}

extension ViewController: UITableViewDataSource {
  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    dataCount
  }
  
  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
    cell.textLabel?.text = "\(indexPath.row)"
    cell.backgroundColor = .systemGray6
    return cell
  }
  
}

extension ViewController: UIScrollViewDelegate {
  func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    var visibleRect = CGRect()
    visibleRect.origin = tableView.contentOffset
    visibleRect.size = tableView.bounds.size
    let visiblePoint = CGPoint(x: visibleRect.midX, y: visibleRect.midY)
    guard let indexPath = tableView.indexPathForRow(at: visiblePoint) else { return }
    currentIndex = indexPath.row
    tableView.scrollToRow(at: indexPath, at: .top, animated: true)
  }
  
  func scrollViewWillEndDragging(_ scrollView: UIScrollView,
                                 withVelocity velocity: CGPoint,
                                 targetContentOffset: UnsafeMutablePointer<CGPoint>)
  {
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
      self.scrollViewDidEndDecelerating(scrollView)
    }
  }
}
カテゴリー
iOS Swift

UICollectionView Programmatically

Sample code

class ViewController: UIViewController {

  var viewWidth: CGFloat {
    return view.bounds.width
  }
  
  var viewHeight: CGFloat {
    return view.bounds.height
  }
  
  lazy var collectionView: UICollectionView = {
    let layout = UICollectionViewFlowLayout()
    layout.sectionInset = UIEdgeInsets(top: 20, left: 10, bottom: 10, right: 10)
    layout.itemSize = CGSize(width: viewWidth*0.8, height: 200)
    let collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
    collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
    collectionView.backgroundColor = .yellow
    
    return collectionView
  }()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    view.backgroundColor = .white
    view.addSubview(collectionView)
    collectionView.delegate = self
    collectionView.dataSource = self
  }
  
  override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()
    
  }
  
  private func layout() {
    collectionView.frame = view.bounds
  }

}

extension ViewController: UICollectionViewDelegate {
}

extension ViewController: UICollectionViewDataSource {
  func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return 5
  }
  
  func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
    cell.backgroundColor = .blue
    return cell
  } 
}
// when you want access to the cells from your custom function

func doSomethinWithTheCell(index: Int) {
  guard let cell = collectionView.cellForItem(at: IndexPath(item: index, section: 0)) else { return }
  cell.backgroundColor = .yellow
}
カテゴリー
iOS Swift

AudioKit パッケージを追加する

毎回手順を忘れて時間をロスするので笑 ここに記録しておきます。。。

File -> Add Packages
Click + button at the left bottom corner -> Add Package Collection
Enter JSON URL

Select the Project in the dropdown -> Add Package

カテゴリー
Mac

M1 Macの環境構築

M1 Macを新たに購入したので、環境構築をしてみました。

お役立ちリンク集

Homebrew

PHP and Composer

MySQL

Laravel

Python

Git

Cocoapods

カテゴリー
iOS RxSwift Swift

UIPickerViewサンプルコード

Github Repo