20211012のSwiftに関する記事は6件です。

横にページングするScrollViewを作成(UIScrollView)

今回の内容 コードと簡単解説 横にだけスクロールさせたいので、.contentSizeをCGSize(width: view.frame.width * 3, height: view.frame.height)を設定します。 ページングさせるので、.isPagingEnabled = trueで設定します。 import UIKit class ViewController: UIViewController { let scrollView = UIScrollView() override func viewDidLoad() { super.viewDidLoad() scrollView.frame = CGRect(x: view.frame.minX, y: view.frame.minY, width: view.frame.width, height: view.frame.height) scrollView.contentSize = CGSize(width: view.frame.width * 3, height: view.frame.height) scrollView.isPagingEnabled = true scrollView.backgroundColor = .systemIndigo view.addSubview(scrollView) let leftView = UIView(frame: CGRect(x: view.frame.minX, y: view.frame.minY, width: view.frame.width, height: view.frame.height)) leftView.backgroundColor = .systemGreen scrollView.addSubview(leftView) let centerView = UIView(frame: CGRect(x: view.frame.maxX, y: view.frame.minY, width: view.frame.width, height: view.frame.height)) centerView.backgroundColor = .systemRed scrollView.addSubview(centerView) let rightView = UIView(frame: CGRect(x: view.frame.maxX * 2, y: view.frame.minY, width: view.frame.width, height: view.frame.height)) rightView.backgroundColor = .systemTeal scrollView.addSubview(rightView) } } 終わり ご指摘、ご質問などありましたら、コメントまでお願い致します。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MVPパターン

MVPパターンとは MVPには2つの種類が存在する。 ①,Passive方式(フロー同期) 絶対にPresentarを通る設計になる。(ViewはModelに直接アクセスできない) データの流れ(Passive方式、フロー同期) 1,ユーザー操作 2,入力、タップなどのイベント 3,Presentarで受け取る(Input) 4,Modelに通知してデータをもらってくる 5,PresentarでUIの更新を指示していく ⇨コードは長くなりそうだけどデータの移動は読みやすそう? フロー同期 Presentarの処理中に何らかのタイミングでViewを更新させていく状態管理の方法 ②,SuperVising方式(フロー同期、オブザーバー同期) 1ユーザー操作 2,入力、タップなどのイベント 3、Presentarで受け取り(Input)Modelに値を渡す 4、データが変更された通知を直接Viewに渡してView自身に更新させる オブザーバー同期:Model の変更通知を受け取り、View自身のロジックで更新していく なぜMVPを実装するのか よく言われるFatViewControllerにならないようにするため. UIの変更やライフサイクルの管理などもViewControllerがしなくてはならないので、可読性が下がってしまうから。 Model 実際にデータの処理を行う。ドメインロジックを担う View Presentarにイベントを渡して(Input)、outputとしてUI更新の指示をもらい更新する。 Passive方式では Viewは基本的に受動的にUI更新の指示を待つだけの存在となる。 また、Modelと直接アクセスしないためModelとViewが疎の状態となっている。 Presentar ModelとViewを仲介する役割を持つ。 プレゼンテーションロジックを担当する。プレゼンテーションロジックはユーザーの挙動によって、アプリケーションの動きを指示していくロジックのこと 感想 MVCに慣れているので違う設計は楽しい! もっとIOS設計について学びたい!! PokemonAPIでポケモン図鑑を作る(MVP,Passive View方式) https://pokeapi.co/ 簡単にPokemonAPIを使用してMVPのアプリを作成しました. コピペではつかえませんが、、、 Model protocol GetPokemonDataInput { func fetchPokemon(completion:@escaping([PokemonModel])->Void) } class PokemonDataModel:GetPokemonDataInput { func fetchPokemon(completion:@escaping([PokemonModel])->Void) { var pokemonArray = [PokemonModel]() let dispatchGroup = DispatchGroup() for i in 1...386 { dispatchGroup.enter() let pokemonURL = "https://pokeapi.co/api/v2/pokemon-species/\(i)" let pokemonDetailURL = "https://pokeapi.co/api/v2/pokemon/\(i)" let keyNumber = i > 251 ? 23 : i > 151 ? 26 : 29 let keyNumber2 = i > 251 ? 31 : i > 151 ? 34 : 37 let keyNumber3 = i > 251 ? 47 : i > 151 ? 50 : 38 AF.request(pokemonURL).responseJSON { response in switch response.result { case .success: do { guard let safeData = response.data else { return } let data = try JSONDecoder().decode(PokemonData.self,from:safeData) let name = data.names[0].name let id = data.id let genera = data.genera[0].genus let explain = data.flavor_text_entries[keyNumber].flavor_text let explain2 = data.flavor_text_entries[keyNumber2].flavor_text let explain3 = data.flavor_text_entries[keyNumber3].flavor_text AF.request(pokemonDetailURL).responseJSON { response in switch response.result { case .success: do { defer { dispatchGroup.leave() } let data = try JSONDecoder().decode(PokemonDetail.self, from: response.data!) let height = data.height let weight = data.weight let imageUrl = data.sprites.front_default let type = data.types[0].type.name let pokemon = PokemonModel(name: name, id: id, genus: genera, explain: explain, explain2: explain2, explain3: explain3, height: height, weight: weight, urlImage: imageUrl,type:type) pokemonArray.append(pokemon) } catch { print(error) } case .failure(let error): print(error) } } } catch { print(error) } case .failure(let error): print(error) } } } dispatchGroup.notify(queue: .main) { completion(pokemonArray) } } } Presentar import Foundation protocol PokemonPresentarInput:AnyObject { var numberOfPokemon:Int { get } var numberOfSavePokemon:Int { get } func didSelectTap(indexPath:IndexPath) func viewDidLoad() func pokemon(row:Int)->PokemonModel? func savePokemon(row:Int)->PokemonModel? func addPokemon(index:Int) func searchTextInput(text:String) func deleteFavorite(index:Int) } protocol PokemonPresentarOutput:AnyObject { func gotoPokemonDetail(pokemon:PokemonModel) func pokemonDataOutPut(pokemon:[PokemonModel]) func filterPokemonOutput(pokemon:[PokemonModel]) } protocol PokemonFavoritePresentarOutput:AnyObject { func deleteComplete() } class PokemonPresentar:PokemonPresentarInput { //Properties private var pokemons = [PokemonModel]() private weak var viewOutput:PokemonPresentarOutput! private var pokemonDataModel:GetPokemonDataInput private var savePokemons = UserDefaultsRepository.loadFromUserDefaults() private weak var favoriteOutput:PokemonFavoritePresentarOutput! var numberOfPokemon: Int { return pokemons.count } var numberOfSavePokemon:Int { return savePokemons.count } //Mark initialize init(viewOutput:PokemonPresentarOutput,modelInput:GetPokemonDataInput) { self.viewOutput = viewOutput self.pokemonDataModel = modelInput } init(favoriteViewOutput:PokemonFavoritePresentarOutput,modelInput:GetPokemonDataInput) { self.favoriteOutput = favoriteViewOutput self.pokemonDataModel = modelInput } //Mark inputMethod func viewDidLoad() { print(#function) pokemonDataModel.fetchPokemon { [weak self] pokemons in guard let self = self else { return } let pokemonArray = pokemons.sorted(by: { $0.id < $1.id }) self.pokemons = pokemonArray self.viewOutput.pokemonDataOutPut(pokemon: self.pokemons) } } func addPokemon(index: Int) { print(#function) self.savePokemons = UserDefaultsRepository.loadFromUserDefaults() self.savePokemons.append(pokemons[index]) UserDefaultsRepository.saveToUserDefaults(pokemon: savePokemons) } func pokemon(row:Int)->PokemonModel? { print(#function) return row >= pokemons.count ? nil:pokemons[row] } func savePokemon(row:Int)->PokemonModel? { print(#function) return row >= savePokemons.count ? nil:savePokemons[row] } func didSelectTap(indexPath:IndexPath) { print(#function) let pokemon = pokemons[indexPath.row] viewOutput.gotoPokemonDetail(pokemon: pokemon) } func searchTextInput(text: String) { print(#function) let filterPokemonArray = self.pokemons.filter { return $0.name.contains(text)} self.pokemons = filterPokemonArray viewOutput.filterPokemonOutput(pokemon: self.pokemons) } func deleteFavorite(index: Int) { self.savePokemons = UserDefaultsRepository.deleteFromUserDefaults(index: index, pokemons: savePokemons) favoriteOutput.deleteComplete() } } View import UIKit class PokemonViewController: UICollectionViewController{ //Properties private let cellId = "cellId" private let headerId = "headerId" private var pokemonPresentar:PokemonPresentarInput! private var indicatorView = UIActivityIndicatorView() private var searchController = UISearchController() //Lifecycle override func viewDidLoad() { super.viewDidLoad() setupCollectionView() pokemonPresentar = PokemonPresentar(viewOutput: self, modelInput: PokemonDataModel()) pokemonPresentar.viewDidLoad() setupSeachController() } //Mark setupMethod private func setupCollectionView() { collectionView.register(PokemonCell.self, forCellWithReuseIdentifier: cellId) collectionView.register(PokemonHeader.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: headerId) indicatorView.center = view.center indicatorView.style = .whiteLarge indicatorView.color = .gray view.addSubview(indicatorView) indicatorView.startAnimating() } //Initialize init() { super.init(collectionViewLayout: UICollectionViewFlowLayout()) } required init?(coder: NSCoder) { fatalError() } //setupMethod private func setupSeachController() { navigationItem.title = "Pokemon Picture Book" searchController = UISearchController(searchResultsController: nil) searchController.searchResultsUpdater = self searchController.obscuresBackgroundDuringPresentation = false navigationItem.searchController = searchController navigationItem.hidesSearchBarWhenScrolling = true } } //Mark collectionViewdelegate Method extension PokemonViewController { override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return pokemonPresentar.numberOfPokemon } override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! PokemonCell guard let pokemon = pokemonPresentar.pokemon(row: indexPath.row) else { return cell} cell.pokemon = pokemon cell.delegate = self return cell } override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: headerId, for: indexPath) as! PokemonHeader header.delegate = self return header } override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { print(#function) pokemonPresentar.didSelectTap(indexPath: indexPath) } } //Mark collectionviewflowlayoutMethod extension PokemonViewController :UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { return CGSize(width: view.frame.width, height: 60) } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { return CGSize(width: view.frame.width, height: 150) } } //Mark PokemonSaveDelegate extension PokemonViewController:PokemonSaveDelegate { func addPokemon(cell: PokemonCell) { print(#function) let indexTapped = collectionView.indexPath(for: cell) guard let index = indexTapped?[1] else { return } let vc = UIAlertController(title: "お気に入りに追加しますか", message: "", preferredStyle: .alert) let alertAction = UIAlertAction(title: "はい", style: .default) { _ in self.pokemonPresentar.addPokemon(index: index) } let cancleAction = UIAlertAction(title: "いいえ", style: .cancel) { _ in print("canle") } vc.addAction(alertAction) vc.addAction(cancleAction) present(vc, animated: true, completion: nil) } } //Mark PokemonHeaderDelegate extension PokemonViewController:PokemonHeaderDelegate { func allFavoritePokemon() { print(#function) let vc = FavoritePokemonController() navigationController?.pushViewController(vc, animated: true) } } //Mark PokemonPresarOutput extension PokemonViewController :PokemonPresentarOutput { func filterPokemonOutput(pokemon: [PokemonModel]) { print(#function) DispatchQueue.main.async { self.collectionView.reloadData() } } func pokemonDataOutPut(pokemon: [PokemonModel]) { print(#function) DispatchQueue.main.async { self.indicatorView.stopAnimating() self.collectionView.reloadData() } } func gotoPokemonDetail(pokemon: PokemonModel) { print(#function) let vc = PokemonDetailController(pokemon: pokemon) navigationController?.pushViewController(vc, animated: true) } } //Mark searchResultUpdating extension PokemonViewController:UISearchResultsUpdating { func updateSearchResults(for searchController: UISearchController) { print(#function) guard let text = searchController.searchBar.text else { return } if !text.isEmpty { pokemonPresentar.searchTextInput(text: text) } else { pokemonPresentar.viewDidLoad() } } }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Xcode13 iOS15でアプリアイコンが表示されないバグについて

はじめに Xcode13・iOS15にアップデートしてから、アプリをXcodeでビルドしてインストールしてもアプリアイコンが反映されなかったので、一時的な方法ではありますが解決方法を残しておきたいと思います。 解決方法 シュミレーターでビルドした場合はインストールしたアプリを一度削除(アプリのアイコンを長押で消せます)してからもう一度ビルドすると反映されます。 実機転送でビルドした場合は、アプリインストール後に端末を再起動すると反映されます。 その他のアプリアイコンの作成などはいつも通りに行っていただいて問題ないと思われます。 iPhoneのバージョンが最新ではない場合はiOS15.0.1からiOS15.0.2にアップデートしてからビルドすると通常通りアプリアイコンの反映がされました。 おそらくiOS15.0.1のバグだったと思われるので、アイコンの反映がされていない場合はまずバージョンが最新か確認をしてから行うと大丈夫そうです。 最後に 簡単な解決方法でしたが、この方法に行き当たるまで30分ほどしどろもどろしたので、同じ問題で悩んでいる方々のお役に立てると嬉しいです。 参考サイト iOS 15 に対応する (Xcode 13.0) Blank App Icon (physical device) - iOS 15 Xcode 13 betas ?参考書籍 SwiftUI対応 たった2日でマスターできるiPhoneアプリ開発集中講座 Xcode 12/iOS 14対応 Swift UI対応 たった2日でマスターできる iPhoneアプリ開発集中講座 Xcode13/iOS15/Swift 5.5対応
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【就活】iOSエンジニア志望の面接で聞かれたことまとめ

はじめに 面接で聞かれたことをまとめていきます。iOSに限らず、エンジニア志望の方に役に立てばいいなと思っています。 具体的な社名などは伏せますし、面接日時や合否は書きません。 「技術」系 「将来」系 「チーム開発」系
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【iOS】続・半強制アップデートの仕組みをカジュアルに実装する

はじめに ユーザの端末にインストールされているアプリのバージョンが古いままだと、最新機能やバグ修正が提供できないだけでなく、メンテナンスが難しくなっていきます。ユーザには常に最新バージョンを使ってもらうのが望ましく、アップデートを促す仕組みは積極的に取り入れた方が良いと考えています。 アプリを起動したとき、最新バージョンがApp Storeにリリースされていることをユーザに伝え、アップデートを促す仕組みをこの記事では半強制アップデートと呼びます。 以前、「【iOS】半強制アップデートの仕組みをカジュアルに実装する」という記事でiTunes Search APIを利用した半強制アップデートの実装を紹介しました。このソースコードは僕が開発しているいくつものアプリに導入されており、これだけ流用するのならばフレームワーク化して一元管理した方が楽になってきます。 ということで、アプリの最新バージョンがApp Storeにリリースされたらアップデートを促す機能を提供するフレームワーク: SwiftyUpdateKitを作りました。なるべく少ないコード量で機能を実装できるように意識して作っています。 ソースコードはGitHubに公開しています。 SwiftyUpdateKitの使い方 インストール方法はCarthage、CocoaPodsに対応しています。ビルド済のxcframeworkもReleasesにアップしてあります。詳しくはGitHubのREADMEを見てください。 初期化 AppDelegateクラスのapplication(_:,didFinishLaunchingWithOptions:)メソッド内でコンフィグを設定します。以下のコードのコメントを参考にしてください。 AppDelegate.swift import SwiftyUpdateKit @main class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { let config = SwiftyUpdateKitConfig( // 現在のアプリバージョン // 普通は以下の通りInfo.plistのバージョンを指定すれば良いはずです version: Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String, // iTunes ID // iTunes IDはブラウザでApp Storeのアプリページを開いたときのURLから分かります // e.g.) App Store URL: "https://apps.apple.com/app/sampleapp/id1234567890" -> iTunesID is 1234567890 iTunesID: "1491913803", // App StoreのアプリページのURL storeURL: "https://apps.apple.com/app/blue-sketch/id1491913803", // iTunes Search APIで使う国コード。省略したときはUSの情報を取得します // 国コードは↓で調べられます // http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 // 多言語対応するときはこの国コードを切り替えてください country: "jp", // アプリバージョンの比較方法 // 省略したときはX.Y.Z形式のバージョンをstoreVersion > currentVersionかで比較します versionCompare: VersionCompare(), // アップデートアラートのタイトル updateAlertTitle: "新しいバージョンがあります!", // アップデートアラートのメッセージ updateAlertMessage: "アプリをアップデートしてください。アップデート内容の詳細はApp Storeを参照してください。", // アップデートアラートの更新ボタン updateButtonTitle: "アップデート", // アップデートアラートのキャンセルボタン // nilを指定したときは非表示 -> キャンセル不可のためアップデートを強制します remindMeLaterButtonTitle: "また後で" ) // コンフィグをセットし初期化 // 第2引数のクロージャをセットしたときはフレームワークの内部ログを出力します SUK.applicationDidFinishLaunching(withConfig: config) { print($0) } return true } } iTunes IDの調べ方はこちらを参考にしてください。 バージョンチェックする 現在のアプリバージョンとApp Storeにリリースされているバージョンを比較するためにはcheckVersionメソッドをviewDidAppearメソッド内で実行します。 ViewController.swift override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) SUK.checkVersion(VersionCheckConditionAlways()) } この一行でバージョン比較を行ない、最新バージョンがApp Storeにリリースされていた場合は以下のようなアップデートアラートを表示します。アラートのテキストはコンフィグで設定したものになります。 checkVersionメソッドの引数のVersionCheckConditionには以下の種類があります。 // 常にバージョンチェックを行ないます VersionCheckConditionAlways() // 一日一回バージョンチェックを行ないます // 1回チェックした後は日付が変わるまでチェックを行ないません VersionCheckConditionDaily() // 常にバージョンチェックを行ないません VersionCheckConditionDisable() この他に独自の条件を指定したい場合はVersionCheckConditionプロトコルを実装し、そのオブジェクトを引数に指定してください。 public protocol VersionCheckCondition: AnyObject { /// If returns true, checks the app version. func shouldCheckVersion() -> Bool } 以前の記事では半強制アップデートの仕組みまでを紹介していました。SwiftyUpdateKitには更に追加の機能を用意しています。 アップデート後にリリースノートを表示する SwiftyUpdateKitを使えば、アプリアップデート後の初回起動時に変更内容を伝えるリリースノートをユーザに表示できます。以下のコードを御覧ください。先程のコードにnewRelease以降が足されています。 ViewController.swift override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) SUK.checkVersion(VersionCheckConditionAlways(), newRelease: { [weak self] newVersion, releaseNotes, firstUpdated in guard let self = self else { return } SUK.showReleaseNotes(from: self, text: releaseNotes, version: newVersion) }) } このコードはアプリのバージョンチェックを行ない最新Ver.であったとき、newReleaseクロージャが呼ばれます。その中でshowReleaseNotesメソッドを実行すると以下のようなViewControllerが表示されます。textに渡しているreleaseNotesは、App StoreのリリースノートをiTunes Search APIで引っ張ってきたものになります。firstUpdatedフラグは、SwiftyUpdateKitを導入して初めてアップデートしたとき(初回インストール含む)のみtrueになります。 showReleaseNotesメソッドの代わりに任意のViewControllerやViewを作って表示しても構いません。(デフォルトのViewControllerはかなりシンプルなので) アプリレビューを要求する もう一つの追加機能は以下のようなアプリレビューを要求できます。 以下のコードは、先程のコードにrequestReviewメソッドが足されています。requestReviewメソッドの引数のRequestReviewConditionは、VersionCheckConditionプロトコルと同様です。ユーザにレビューをお願いする条件をカスタマイズできます。 ViewController.swift override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) SUK.checkVersion(VersionCheckConditionAlways(), newRelease: { [weak self] newVersion, releaseNotes, firstUpdated in guard let self = self else { return } SUK.showReleaseNotes(from: self, text: releaseNotes, version: newVersion) }) { SUK.requestReview(RequestReviewConditionAlways()) } } このコードは、 現在のアプリバージョンが最新でないならば、アップデートアラートを表示 最新Ver.にアプリがアップデートされた後、初めて起動された時にリリースノートを表示 最新Ver.のアプリであり、リリースノートも表示済みならば、レビューを要求 という挙動になります。 まとめ SwiftyUpdateKitを使えば、簡単に以下の機能をアプリに組み込むことができます。 アプリの最新バージョンがApp Storeにリリースされたらアップデートを促す アプリアップデート後の初回起動時に変更内容を伝えるリリースノートをユーザに表示 任意の条件でユーザにレビューを要求
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vapor】PermissionErrorでファイルが開けない

概要 vapor new testProj cd testProj vapor xcode 上記コマンドで新規プロジェクトを作成してXcodeで開くとPermissionErrorが発生してファイルが1つも見れない 試したこと chmodで権限を変更 → 解決せず 解決策 Macのシステム環境設定 → セキュリティとプライバシー → プライバシー → ファイルとフォルダからターミナルとXcodeのデスクトップフォルダへのアクセスを許可する これで無事に開けるようになりました
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む