- 投稿日:2020-07-29T21:43:06+09:00
[swift5]Protocolの作成方法
実装のポイント
Protocolの作成方法を5つの項目に分けて言語化及び要約してアウトプットしていきます。
初学者の方(私も初学者ですがw)はProtocolに関してわからない部分が多いと思いますが、実装の流れを掴む事ができればコードは書ける(Xcodeのおかげw)と思いますので実装の流れに注目して下さい!今回は
ViewController
とNextViewController
の2画面が存在し、
NextViewControllerからViewControllerへメソッドを委任したいと思います。環境
swift5
Xcode 11①Protocolの作成
まず
nextViewController
で下記コードのようにProtocolの定義と、Protocolで使用するメソッドの宣言を行います。今は使用するメソッドの規則だけでOKです。NextViewController.swiftprotocol プロトコル名ProtocolDelegate { //規則のみ決める func メソッド名(委任したい値: 型) } //プロトコルを変数に代入して宣言(? = nil NG) var 変数名: プロトコル名ProtocolDelegate?②他クラスで発動(戻るボタンpushで発動の場合)
今回は戻るボタンpushで発動する場合のコードを記述します。
NextViewController.swift@IBAction func backButton(_ sender: Any) { //delegateメソッドを任せたクラスで発動 ①で宣言した変数名.メソッド名( 委任したい値: 値(変数名) ) //ボタンを押して戻る処理 dismiss(animated: true, completion: nil) }③使用(委任)するクラスにProtocolで宣言
ViewController
のclass宣言箇所に①で宣言したプロトコル名ProtocolDlegate
を記述。するとエラーが発生するので'Fix'を選択してXcodeの補完に従い、④のようにメソッドを作成する。
ViewController.swiftclass ViewController: UIViewController,プロトコル名ProtocolDelegate { }④Protocolで書かれたメソッドを記述
③で自動補完されるメソッド。
ViewController.swiftfunc ①で宣言した規則のメソッド名(委任したい値: 型) { }⑤NextViewControllerからViewControllerへ値を受け取る場合
ViewController.swiftoverride func prepare(for segue: UIStoryboardSegue, sender: Any?) { //変数nextVCに委任したい側のコントローラー名を代入し値を渡す let nextVC = segue.destination as! NextViewController //委任完了コード nextVC.delegate = self }最後に
もし誤っている箇所や疑問がある場合はコメント欄にて受け付けておりますのでご連絡お待ちしております。是非参考にしてください!
- 投稿日:2020-07-29T19:12:14+09:00
MacでHEICファイルを一斉変換
iPhoneから写真を共有してもらった時にHEICファイルだとまだ取り回しが悪いのでjpegへ変換する。一個くらいならプレビューでjpegエクスポートでもいいけど面倒なので一斉変換。
for f in *.HEIC ; do sips --setProperty format jpeg $f --out `basename -s .HEIC $f`.jpg; donesips知らなかった...
jpegのサイズをもう少し下げたい場合はsipsのoptionでlow指定する。これで十分。for f in *.HEIC ; do sips -s format jpeg -s formatOptions low $f --out `basename -s .HEIC $f`.jpg; done
- 投稿日:2020-07-29T17:58:33+09:00
PNG の IDAT のみを弄る
Swift の練習を兼ねて PNG ファイルを弄るプログラムを作ってみる。
処理内容は
- PNG ファイルをチャンク単位にして読み込む
- IDAT 以外は変更しない
- IDAT を zlib で展開 (uncompress)
- フィルタ タイプを弄る
- IDAT を zlib で圧縮 (compress)
- PNG ファイルを出力する
で、IDAT 内のフィルタ タイプの処理をテストする。
テスト プログラム(Xcode のコンソールアプリ)
main.swiftimport Foundation import Compression // ############ var flag_update_image = true var flag_image_filter = true var flag_overwrite = false var flag_verbose = false // ############ enum PNGError : Error { case system case open case exists case create case signature case unsupported case data case io func errorPrint(_ msg: String) { switch (self) { case .system: print("内部エラー: \(msg)") case .open: print("ファイルが開けません: \(msg)") case .exists: print("ファイルが既にあります: \(msg)") case .create: print("ファイルが作れません: \(msg)") case .signature: print("PNG ではありません: \(msg)") case .unsupported: print("非対応の形式です: \(msg)") case .data: print("データが破損しています: \(msg)") case .io: print("読み込みに失敗しました: \(msg)") } } } // ############ func UInt32EB(_ byteArray: ArraySlice<UInt8>) -> UInt32 { var value: UInt32 = 0 for b in byteArray { value <<= 8 value |= UInt32(b) } return value } extension Data { func ToArrayU8() -> [UInt8] { return [UInt8](unsafeUninitializedCapacity: count) { buffer, initializedCount in copyBytes(to: buffer, from: 0..<count) initializedCount = count } } } // ############ extension FileHandle { func readDataU8(ofLength count: Int) -> [UInt8] { return readData(ofLength: count).ToArrayU8() } func readU32EB() -> UInt32? { let data = readDataU8(ofLength: 4) if data.count != 4 { return nil } return UInt32EB(data[0..<4]) } func writeU32EB(_ value: UInt32) { write(Data([ UInt8((value >> 24) & 0xff), UInt8((value >> 16) & 0xff), UInt8((value >> 8) & 0xff), UInt8((value >> 0) & 0xff), ])) } } // ############ class CRC32Table { private static var dictionary: [UInt32: [UInt32]] = [:] private let table: [UInt32] subscript(index: Int) -> UInt32 { return table[index] } init(_ polynomial: UInt32) { if let dictTable = CRC32Table.dictionary[polynomial] { table = dictTable return } let newTable = [UInt32](unsafeUninitializedCapacity: 256) { buffer, initializedCount in for n in 0..<256 { var v = UInt32(n) for _ in 0..<8 { let f = v & 1 v >>= 1 if f != 0 { v ^= polynomial } } buffer[n] = v } initializedCount = 256 } CRC32Table.dictionary[polynomial] = newTable table = newTable } } class CRC32 { private let table: CRC32Table private var value_: UInt32 = ~0 var value : UInt32 { get { return ~value_ } } init(_ polynomial: UInt32 = 0xEDB88320) { table = CRC32Table(polynomial) } func update(_ data: UInt8) { let index = Int(UInt8(value_ & 0xff) ^ data) value_ = (value_ >> 8) ^ table[index] } func update(_ data: Data) { update(data.ToArrayU8()) } func updateEB(_ data: UInt32) { update(UInt8((data >> 24) & 0xff)) update(UInt8((data >> 16) & 0xff)) update(UInt8((data >> 8) & 0xff)) update(UInt8((data >> 0) & 0xff)) } func update(_ data: [UInt8]) { for v in data { update(v) } } } // ############ // Compression Module (ZLIB) func CompressM(_ src: [UInt8]) -> [UInt8]? { var buffer: [UInt8] = [0x78, 0x5E] do { let out = try OutputFilter(.compress, using: .zlib) { (data: Data?) -> Void in buffer.append(contentsOf: data?.ToArrayU8() ?? []) } try out.write(src) try out.finalize() } catch let error { print(error.localizedDescription) return nil } return buffer } func ZLibUncompress(_ size: UInt32 , _ src: [UInt8]) -> [UInt8]? { var buffer: [UInt8] = [] do { let out = try OutputFilter(.decompress, using: .zlib) { (data: Data?) -> Void in buffer.append(contentsOf: data?.ToArrayU8() ?? []) } try out.write(src[2...]) try out.finalize() } catch let error { print(error.localizedDescription) return nil } return buffer } // C Library func zlibCompress(_ src: [UInt8]) -> [UInt8]? { var buffer = [UInt8](repeating: 0, count: Int(src.count * 2)) let status = CompressC(&buffer, UInt(buffer.count), src, UInt(src.count), 9) if status < 0 { return nil } return [UInt8](buffer[0..<status]) } // https://developer.apple.com/documentation/compression/compression_zlib // Compression モジュールは圧縮レベル 5 のみだから高圧縮を求めるため C ライブラリを使う. var ZLibCompress = zlibCompress // C ライブラリ (libz) を使用する. // ############ class Chunk { var length: UInt32 = 0 var idData = Data() var id = "" var data: [UInt8] = [] var crc: UInt32 = 0 init() { /*NOP*/ } init(_ handle: FileHandle) throws { try read(handle) } func read(_ handle: FileHandle) throws { guard let rLen = handle.readU32EB() else { throw PNGError.io } length = rLen idData = handle.readData(ofLength: 4) if idData.count != 4 { throw PNGError.io } guard let cId = String(data: idData, encoding: String.Encoding.utf8) else { throw PNGError.data } id = cId let rData = handle.readDataU8(ofLength: Int(rLen)) if rData.count != rLen { throw PNGError.data } data = rData guard let rCrc = handle.readU32EB() else { throw PNGError.io } crc = rCrc let crc32 = CRC32() crc32.update(idData) crc32.update(data) if crc32.value != rCrc { throw PNGError.data } VerbosePrint(String(format: "\(id): len=%-6d crc=%#010x", length, crc)) } func write(_ handle: FileHandle) { handle.writeU32EB(length) handle.write(idData) handle.write(Data(data)) let crc32 = CRC32() crc32.update(idData) crc32.update(data) handle.writeU32EB(crc32.value) VerbosePrint(String(format: "\(id): len=%-6d crc=%#010x", length, crc)) } } class IHDRChunk { static let colorTypeTable: [UInt8] = [1,0,3,1,2,0,4] var width: UInt32 = 0 var height: UInt32 = 0 var colorDepth: UInt8 = 0 var colorType: UInt8 = 0 var compression: UInt8 = 0 var filter: UInt8 = 0 var interlace: UInt8 = 0 var bitsPerPixel: UInt8 = 0 var bytesPerPixel : UInt8 = 0 init() { /*NO-OP*/ } init(_ chunk: Chunk) { let ihdr = chunk.data width = UInt32EB(ihdr[0..<4]) height = UInt32EB(ihdr[4..<8]) colorDepth = ihdr[8] colorType = ihdr[9] compression = ihdr[10] filter = ihdr[11] interlace = ihdr[12] let numDepth = IHDRChunk.colorTypeTable[Int(colorType)] bitsPerPixel = colorDepth * numDepth bytesPerPixel = (bitsPerPixel + 7) >> 3 } } struct ADAM7Param { static let xOffsetTable: [UInt8] = [0, 4, 0, 2, 0, 1, 0, 0] static let yOffsetTable: [UInt8] = [0, 0, 4, 0, 2, 0, 1, 0] static let xStepTable : [UInt8] = [8, 8, 4, 4, 2, 2, 1, 1] static let yStepTable : [UInt8] = [8, 8, 8, 4, 4, 2, 2, 1] let xOffs: UInt32 let yOffs: UInt32 let xStep: UInt32 let yStep: UInt32 let width: UInt32 let height: UInt32 let rawBytes: UInt32 let stride: UInt32 let imageSize: UInt32 let imageOffset: UInt32 init(_ ihdr: IHDRChunk, _ level:Int, _ offs:UInt32) { xOffs = UInt32(ADAM7Param.xOffsetTable[level]) yOffs = UInt32(ADAM7Param.yOffsetTable[level]) xStep = UInt32(ADAM7Param.xStepTable[level]) yStep = UInt32(ADAM7Param.yStepTable[level]) // width = (ihdr.width >= xOffs) ? ((ihdr.width - xOffs + xStep - 1) / xStep) : 0 height = (ihdr.height >= yOffs) ? ((ihdr.height - yOffs + yStep - 1) / yStep) : 0 rawBytes = (width * UInt32(ihdr.bitsPerPixel) + 7) >> 3 stride = (rawBytes != 0) ? (rawBytes + 1) : 0 // imageSize = stride * height imageOffset = offs } } class ADAM7 { let interlace: [ADAM7Param] let interlaceSize: UInt32 let imageSize: UInt32 var progressive: ADAM7Param { get { return interlace[7] } } subscript(index: Int) -> ADAM7Param { return interlace[index] } init(_ ihdr: IHDRChunk) { interlace = [ADAM7Param](unsafeUninitializedCapacity: 8) { buffer, initializedCount in var offs: UInt32 = 0 for n in 0..<7 { buffer[n] = ADAM7Param(ihdr, n, offs) offs += buffer[n].imageSize } buffer[7] = ADAM7Param(ihdr, 7, 0) initializedCount = 8 } interlaceSize = interlace[6].imageOffset + interlace[6].imageSize imageSize = (ihdr.interlace != 0) ? interlaceSize: interlace[7].imageSize } } class PNGFile { static let signature: [UInt8] = [137, 80, 78, 71, 13, 10, 26, 10] var chunk: [Chunk] = [] var idat_length: [UInt32] = [] var idat_average: UInt32 = 0 var ihdr : IHDRChunk = IHDRChunk() var image: [UInt8] = [] subscript(id: String) -> Chunk? { let n = find(id) if n < 0 { return nil } return chunk[n] } init(_ path: String) throws { try self.read(path) } func find(_ id: String) -> Int { for n in 0..<chunk.count { if id == chunk[n].id { return n } } return -1 } func setImageData(_ image: [UInt8]) { let n = find("IDAT") chunk[n].length = UInt32(image.count) chunk[n].data = image } func read(_ handle: FileHandle) throws { let signature: [UInt8] = handle.readDataU8(ofLength: 8) if signature != PNGFile.signature { throw PNGError.signature } var rChunk = try Chunk(handle) ihdr = IHDRChunk(rChunk) var chunk_id = rChunk.id if chunk_id != "IHDR" { throw PNGError.data } chunk.append(rChunk) while chunk_id != "IEND" { let last_chunk = chunk_id rChunk = try Chunk(handle) chunk_id = rChunk.id switch (chunk_id) { case "IHDR": throw PNGError.data case "IDAT": idat_length.append(rChunk.length) if last_chunk == "IDAT" { let idatIdx = chunk.count - 1 chunk[idatIdx].data.append(contentsOf: rChunk.data) chunk[idatIdx].length = UInt32(chunk[idatIdx].data.count) continue } if idat_length.count != 1 { throw PNGError.data } default: break } chunk.append(rChunk) } if ![1,2,4,8,16].contains(ihdr.colorDepth) { throw PNGError.unsupported } if ![0,2,3,4,6].contains(ihdr.colorType) { throw PNGError.unsupported } if ihdr.compression != 0 { throw PNGError.unsupported } if ihdr.filter != 0 { throw PNGError.unsupported } if ihdr.interlace >= 2 { throw PNGError.unsupported } idat_length.removeLast() idat_average = 0 if idat_length.count != 0 { let split1 = idat_length.reduce(0, +) idat_average = split1 / UInt32(idat_length.count) } } func read(_ path: String) throws { guard let handle = FileHandle(forReadingAtPath: path) else { throw PNGError.open } try! self.read(handle) handle.closeFile() } func write(_ handle: FileHandle) { handle.write(Data(PNGFile.signature)) for ch in chunk { if ch.id != "IDAT" { ch.write(handle) continue } if idat_average == 0 { ch.write(handle) continue } var idat_spIdx = 0 var idat_offset = 0 var idat_total = ch.data.count while idat_total > 0 { var idat_split = Int(idat_average) if idat_spIdx < idat_length.count { idat_split = Int(idat_length[idat_spIdx]) idat_spIdx += 1 } if idat_split > idat_total { idat_split = idat_total } let idat_next = idat_offset + idat_split let split_data = Data(ch.data[idat_offset..<idat_next]) handle.writeU32EB(UInt32(idat_split)) handle.write(ch.idData) handle.write(split_data) let crc32 = CRC32() crc32.update(ch.idData) crc32.update(split_data) let crc = crc32.value handle.writeU32EB(crc) idat_offset = idat_next idat_total -= idat_split VerbosePrint(String(format: "\(ch.id): len=%-6d crc=%#010x", split_data.count, crc)) } } } func write(_ path: String, _ overwrite: Bool = false) throws { if FileManager.default.fileExists(atPath: path) { if !overwrite { throw PNGError.exists } try FileManager.default.removeItem(atPath: path) } if !FileManager.default.createFile(atPath: path, contents: nil, attributes: nil) { throw PNGError.system } guard let handle = FileHandle(forWritingAtPath: path) else { throw PNGError.open } self.write(handle) handle.closeFile() } } // ############ func ImageReconstruct(_ ihdr: IHDRChunk, _ adam7: ADAM7, _ image: inout [UInt8]) throws { VerbosePrint("Reconstruct:") for adam in adam7.interlace[(ihdr.filter != 0) ? (0..<7) : (7..<8)] { VerbosePrint(String(format: " (W:%04d, H:%04d)", adam.width, adam.height)) VerbosePrint(String(format: " (X:%04d, Y:%04d)", adam.xOffs, adam.yOffs)) VerbosePrint(String(format: " (R:%04d, B:%04d)", adam.xStep, adam.yStep)) let pixelBytes = Int(ihdr.bytesPerPixel) let stride = Int(adam.stride) let rawBytes = Int(adam.rawBytes) let lineBytes = pixelBytes + stride - 1 var lineOffs = Int(adam.imageOffset) var imgOffs = lineOffs + 1 if stride == 0 { continue } var lastLine = [UInt8](repeating: 0, count: pixelBytes + lineBytes) for y in 0..<adam.height { var currentLine = [UInt8](repeating: 0, count: pixelBytes) currentLine.append(contentsOf: image[imgOffs..<(imgOffs+rawBytes)]) switch (image[lineOffs]) { case 0: // NONE VerbosePrint(String(format: " Line %4d: NONE", y)) break case 1: // SUB VerbosePrint(String(format: " Line %4d: SUB", y)) image[lineOffs] = 0 for x in 0..<rawBytes { let a = UInt16(currentLine[x]) let p = UInt16(currentLine[x+pixelBytes]) let q = UInt8((p + a) & 0xff) image[imgOffs+x] = q currentLine[x+pixelBytes] = q } case 2: // UP VerbosePrint(String(format: " Line %4d: UP", y)) image[lineOffs] = 0 for x in 0..<rawBytes { let b = UInt16(lastLine[x+pixelBytes]) let p = UInt16(currentLine[x+pixelBytes]) let q = UInt8((p + b) & 0xff) image[imgOffs+x] = q currentLine[x+pixelBytes] = q } case 3: // AVE VerbosePrint(String(format: " Line %4d: AVE", y)) image[lineOffs] = 0 for x in 0..<rawBytes { let a = UInt16(currentLine[x]) let b = UInt16(lastLine[x+pixelBytes]) let p = UInt16(currentLine[x+pixelBytes]) let q = UInt8((p + ((a + b) >> 1)) & 0xff) image[imgOffs+x] = q currentLine[x+pixelBytes] = q } case 4: // PAETH VerbosePrint(String(format: " Line %4d: PAETH", y)) image[lineOffs] = 0 for x in 0..<rawBytes { let a = Int16(currentLine[x]) let b = Int16(lastLine[x+pixelBytes]) let c = Int16(lastLine[x]) let p = Int16(currentLine[x+pixelBytes]) let d = a + b - c let pa = (d > a) ? (d - a) : (a - d) var pb = (d > b) ? (d - b) : (b - d) let pc = (d > c) ? (d - c) : (c - d) var qa = a var qb = b if pb > pc { qb = c; pb = pc } if pa > pb { qa = qb } let q = UInt8((p + qa) & 0xff) image[imgOffs+x] = q currentLine[x+pixelBytes] = q } default: throw PNGError.data } lastLine = currentLine lineOffs += stride imgOffs += stride } } } func ImageFilterHint(_ data: [UInt8]) -> UInt32 { var hist = [UInt32](repeating: 0, count: 256) for n in data { hist[Int(n)] += 1 } return hist.reduce(0) { ($0 > $1) ? $0 : $1 } } let filterName = ["NONE", "SUB", "UP", "AVE", "PAETH"] func ImageFilter(_ ihdr: IHDRChunk, _ adam7: ADAM7, _ image: inout [UInt8]) { VerbosePrint("Filter:") for adam in adam7.interlace[(ihdr.filter != 0) ? (0..<7) : (7..<8)] { VerbosePrint(String(format: " (W:%04d, H:%04d)", adam.width, adam.height)) VerbosePrint(String(format: " (X:%04d, Y:%04d)", adam.xOffs, adam.yOffs)) VerbosePrint(String(format: " (R:%04d, B:%04d)", adam.xStep, adam.yStep)) let pixelBytes = Int(ihdr.bytesPerPixel) let stride = Int(adam.stride) let rawBytes = Int(adam.rawBytes) let lineBytes = pixelBytes + stride - 1 var lineOffs = Int(adam.imageOffset) var imgOffs = lineOffs + 1 if stride == 0 { continue } var lastLine = [UInt8](repeating: 0, count: pixelBytes + lineBytes) for y in 0..<adam.height { let pixelData = image[imgOffs..<(imgOffs+rawBytes)] var currentLine = [UInt8](repeating: 0, count: pixelBytes) currentLine.append(contentsOf: pixelData) var f0: [UInt8] = [0] var f1: [UInt8] = [1] var f2: [UInt8] = [2] var f3: [UInt8] = [3] var f4: [UInt8] = [4] // NONE f0.append(contentsOf: pixelData) // SUB for x in 0..<rawBytes { let a = Int16(currentLine[x]) let p = Int16(currentLine[x+pixelBytes]) let q = UInt8((p - a) & 0xff) f1.append(q) } // UP for x in 0..<rawBytes { let b = Int16(lastLine[x+pixelBytes]) let p = Int16(currentLine[x+pixelBytes]) let q = UInt8((p - b) & 0xff) f2.append(q) } // AVE for x in 0..<rawBytes { let a = Int16(currentLine[x]) let b = Int16(lastLine[x+pixelBytes]) let p = Int16(currentLine[x+pixelBytes]) let q = UInt8((p - ((a + b) >> 1)) & 0xff) f3.append(q) } // PAETH for x in 0..<rawBytes { let a = Int16(currentLine[x]) let b = Int16(lastLine[x+pixelBytes]) let c = Int16(lastLine[x]) let p = Int16(currentLine[x+pixelBytes]) let d = a + b - c let pa = (d > a) ? (d - a) : (a - d) var pb = (d > b) ? (d - b) : (b - d) let pc = (d > c) ? (d - c) : (c - d) var qa = a var qb = b if pb > pc { qb = c; pb = pc } if pa > pb { qa = qb } let q = UInt8((p - qa) & 0xff) f4.append(q) } let filters = [f0,f1,f2,f3,f4] var hist: UInt32 = 0 var filter: Int = 0 for n in 0..<5 { let fh = ImageFilterHint(filters[n]) if fh > hist { hist = fh filter = n } } VerbosePrint(String(format: " Line %4d: \(filterName[filter])", y)) let filterData = filters[filter] for n in 0..<stride { image[lineOffs+n] = filterData[n] } lastLine = currentLine lineOffs += stride imgOffs += stride } } } func UpdateImage(_ png: inout PNGFile) throws { guard let idat = png["IDAT"] else { throw PNGError.data } let ihdr = png.ihdr let adam7 = ADAM7(ihdr) guard var image_buffer = ZLibUncompress(adam7.imageSize, idat.data) else { throw PNGError.data } VerbosePrint(String(format: "Uncompress: %d(%d)", image_buffer.count, adam7.imageSize)) if flag_update_image { try ImageReconstruct(ihdr, adam7, &image_buffer) if flag_image_filter { ImageFilter(ihdr, adam7, &image_buffer) } } guard let deflate_buffer = ZLibCompress(image_buffer) else { throw PNGError.system } VerbosePrint(String(format: "Compress: %d -> %d", idat.data.count, deflate_buffer.count)) if flag_update_image { png.setImageData(deflate_buffer) } } // ############ func VerbosePrint(_ msg: String) { if flag_verbose { print(msg) } } // ############ var cmd_args = CommandLine.arguments var program = cmd_args.removeFirst() var program_paths = program.split(separator: "/") var program_name = program_paths.last func cmd_arg_parse() { while cmd_args.count > 0 { var arg = cmd_args[0] if arg.removeFirst() != "-" { break } cmd_args.removeFirst() while arg.count > 0 { switch (arg.removeFirst()) { case "O": flag_overwrite = true case "u": flag_update_image = false case "v": flag_verbose = true case "f": flag_image_filter = false case "M": ZLibCompress = CompressM default: usage() } } } if cmd_args.count < 2 { usage() } } cmd_arg_parse() func usage() -> Never { print(""" Usage: \(String(describing: program_name)) [オプション] 入力ファイル 出力ファイル オプション: -O 既存ファイルへ上書きする -u IDAT を更新しない(複写になる) -v 冗長出力モード IDAT 更新オプション: -f フィルターなしにする -M Compression モジュールを使う """) exit(1) } func main(_ inpFile: String, _ outFile: String) -> Int32 { var path = inpFile do { var png = try PNGFile(inpFile) try UpdateImage(&png) path = outFile try png.write(outFile, flag_overwrite) } catch let error { let rError = error as! PNGError rError.errorPrint(path) return 2 } return 0 } exit(main(cmd_args[0], cmd_args[1]))zlibwrapper.m#import <Foundation/Foundation.h> #import <zlib.h> long CompressC(void *buffer, unsigned long length, const void *data, unsigned long size, int level) { unsigned long bufsz = length; if (compress2((Bytef*)buffer, &bufsz, (const Bytef*)data, size, level) != 0) return -1; return bufsz; }project-Bridging-Header.hlong CompressC(void *buffer, unsigned long length, const void *data, unsigned long size, int level);幾つか試した限りでは、データが小さくなった。uncompress → compress (Lv:9) の処理だけでも小さくなるので、書き出したアプリでの圧縮レベルが最大ではないのだろう。更にフィルタ タイプを弄ると僅かに縮むので、悪い選択処理ではなさそうです。
- 投稿日:2020-07-29T14:23:44+09:00
Azure Computer Vision API-v3.0(OCR) をSwiftで
概要
Azure Computer Vision APIをSwiftにてサンプルコードを作成しました。
リファレンスにてObjCはあるもののSwiftがないため参考になれば幸いです。APIをcallするだけなので、どの言語でもできます
環境
Swift 5
xcode11Podfilepod 'Alamofire', '~> 4.7.3'事前準備
GUIにて
subscription key
とendpoint
を取得サンプルコード
ViewControllerprivate func executeOCR() { // imageViewにて表示しているものをOCR処理する想定 guard let image = self.imageView.image else { return } // それぞれ取得したものをStringで let subscriptionKey = "subscription key" let endpoint = "endpoint" let urlString = endpoint + "vision/v3.0/ocr" var request = URLRequest(url: URL(string: urlString)!) request.addValue(subscriptionKey, forHTTPHeaderField: "Ocp-Apim-Subscription-Key") // languageは適切なものに変更 let params: [String: String] = [ "language":"ja", "detectOrientation":"true" ] Alamofire.upload(multipartFormData: { multipartFormData in var compressionQuality: CGFloat = 1.0 if var imageData = image.jpegData(compressionQuality: compressionQuality) { var imageDataCount = imageData.count repeat { imageData = image.jpegData(compressionQuality: compressionQuality)! imageDataCount = imageData.count compressionQuality -= 0.1 } while imageDataCount > 4000000 multipartFormData.append(imageData, withName: "image", fileName: "file.jpeg", mimeType: "image/jpeg") } for (key, value) in params { multipartFormData.append(value.data(using: .utf8)!, withName: key) } }, with: request) { encodingResult in switch encodingResult { case.success(let upload, _ ,_): upload .responseData { responseData in let parsedResponse = try? JSONDecoder().decode(OCRResponse.self, from: responseData.data!) // 後述 let output = self.getResponseWord(response: parsedResponse) print(output) } case.failure(let _): break } } }OCRResponsestruct OCRResponse: Codable { var language: String? var orientation: String? var regions: [Regions]? // textAngleはIntのはずだがStringで返される時もあったため消しています } struct Regions: Codable { var boundingBox: String? var lines: [Lines]? } struct Lines: Codable { var boundingBox: String? var words: [Words?] } struct Words: Codable { var boundingBox: String? var text: String? }iPhoneの画像拡張子事情
SS:
.png
カメラ:.heic
Q:
・・・heic?A:
iOS11から実装されていたもので「HEIF」形式の拡張子、それまでは「JPEG」形式
この形式は「高画質のまま軽量化」した写真の保存形式
その拡張子が.heic
一部コード解説
repeat
repeat { imageData = image.jpegData(compressionQuality: compressionQuality)! imageDataCount = imageData.count compressionQuality -= 0.1 } while imageDataCount > 4000000リファレンスにも記載があるが、4MBが上限となっている。
HEIF形式で保存される.heic
ですら4MBを超えることがある。(せっかく軽量化しているのに・・・
そのため、4MBを超えるようであれば10%ずつ質をさげている。updataメソッド
Alamofire.uploadこれはDefaultでHTTPメソッドがPOSTで行ってくれる
getResponseWord
private func getResponseWord(response: OCRResponse?) -> String { guard let response = response else { return "" } var output = "" response.regions?.forEach { regions in regions.lines?.forEach { lines in lines.words.forEach { words in output += words?.text ?? "" } output += "\n" } } }ここで各自必要なものを取得。
上記サンプルは1ワード毎に改行したものを返している。補足
無料期間は30日
文字読み取れて嬉しい質問等あればお気軽に!
速さ重視で作ったのでOptinal関連やrepeat文なぜ?等は目を瞑って下さい。
- 投稿日:2020-07-29T13:32:48+09:00
AutoLayoutで使えそうなUIViewのConstraintsExtension
Contraint用Extensionがおすすめ
コードベースで書いた経験が少なかったため、Constraintをどうするか調べていたところ、とても参考になる記事がありました。
一部自分で手を加えましたが、とても使い勝手が良いです。extension UIView{ func anchor(top: NSLayoutYAxisAnchor? = nil, left: NSLayoutXAxisAnchor? = nil, bottom: NSLayoutYAxisAnchor? = nil, right: NSLayoutXAxisAnchor? = nil, paddingTop: CGFloat = 0, paddingLeft: CGFloat = 0, paddingBottom: CGFloat = 0, paddingRight: CGFloat = 0, width: CGFloat? = nil, height: CGFloat? = nil) { translatesAutoresizingMaskIntoConstraints = false if let top = top { topAnchor.constraint(equalTo: top, constant: paddingTop).isActive = true } if let left = left { leftAnchor.constraint(equalTo: left, constant: paddingLeft).isActive = true } if let bottom = bottom { bottomAnchor.constraint(equalTo: bottom, constant: -paddingBottom).isActive = true } if let right = right { rightAnchor.constraint(equalTo: right, constant: -paddingRight).isActive = true } if let width = width { widthAnchor.constraint(equalToConstant: width).isActive = true } if let height = height{ heightAnchor.constraint(equalToConstant: height).isActive = true } } func center(inView view: UIView, yConstant: CGFloat? = 0) { translatesAutoresizingMaskIntoConstraints = false centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: yConstant!).isActive = true } func centerX(inView view: UIView, topAnchor: NSLayoutYAxisAnchor? = nil, paddingTop: CGFloat? = 0) { translatesAutoresizingMaskIntoConstraints = false centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true if let topAnchor = topAnchor { self.topAnchor.constraint(equalTo: topAnchor, constant: paddingTop!).isActive = true } } func centerY(inView view: UIView, leftAnchor: NSLayoutXAxisAnchor? = nil, paddingleft: CGFloat? = nil, constant: CGFloat? = 0) { centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: constant!).isActive = true if let leftAnchor = leftAnchor, let padding = paddingleft { self.leftAnchor.constraint(equalTo: leftAnchor, constant: padding).isActive = true } } func setDimensions(width: CGFloat, height: CGFloat) { translatesAutoresizingMaskIntoConstraints = false widthAnchor.constraint(equalToConstant: width).isActive = true heightAnchor.constraint(equalToConstant: height).isActive = true } func addConstraintsToFillView(_ view: UIView) { translatesAutoresizingMaskIntoConstraints = false anchor(top: topAnchor, left: view.leftAnchor, bottom: bottomAnchor, right: view.rightAnchor) } }参考記事
https://medium.com/@suj9763/auto-layout-extension-in-swift-programmatically-fe1e7cca13e5
- 投稿日:2020-07-29T11:59:36+09:00
アプリ審査通過後App Store へ”App入手不可能 このAppは現在、この国または地域では入手できません”
The status of your app, 〇〇, is now "Ready for Sale"
というメールをいただき、アプリの審査通過ということで高鳴る鼓動を抑えながらApp Storeへ。
すると
という表示...涙
App Store Connect⇨該当のアプリ⇨価格及び配信状況⇨選択されたすべての国または地域を確認。
↓
うーん、ちゃんと日本は選択されている。
↓
App Supportに問合せ。
↓
Ready for Saleから最大24時間App Storeの反映に時間がかかるとの回答。
↓
少ししてもう一度確認するとちゃんと表示されてました♪ちなみにこんなアプリです。最後に
個人的に急ぎだったので、早とちりしてしまいました。
この件については、わざわざqiitaに書くまでもないかなとか思いましたが、丁寧に担当していただいたApp Supportの方への感謝したかったのと、自分と同じような慌てん坊さんの助けになればと思い書きました。
- 投稿日:2020-07-29T10:43:48+09:00
[AtCoder]Swiftでも競プロがしたい!
普段はiOS/Androidのアプリ開発をしている自分ですが、ここ最近競技プログラミングの人気が爆上がりしている1 という噂は聞いており、やろうかなと思っていた所...
AtCoderでSwift5.2が使える様になった との情報を耳にしたのでこの機に競技プログラミングを初めてみました!そうです!Swiftはできる子なんだってところを競技プログラミング界に知らしめてやりましょう!
この記事では、 AtCoderをSwiftで初めてみた体験記 と、 Swiftを使っている方々が競技プログラミングを始めやすいように解説 の二つを兼ねて、AtCoder&競技プログラミングの導入から Swiftを使ってコンテストに参加する までを紹介していきます!
本記事では、AtCoderホームページのスクリーンショットの掲載や問題文の引用を行っています。それらはこの記事の著者の著作物ではなく、一切の権利はAtCoderに帰属します。
AtCoderへの登録
公式サイトにアクセスして、右上の「新規登録」からアカウントを登録します
- 公式サイト: https://atcoder.jp/
コンテストの流れを体験する
さて、登録ができたところで
AtCoderに用意されている、練習用コンテストからコンテストの流れを確認してみましょう
今後のコンテストや過去問を解く場合と同じ手順になっています。
- 練習用コンテスト: https://atcoder.jp/contests/practice
「参加登録」ボタンをクリックすると、上のタブに「問題」が追加されるのでクリックします。
クリックすると、問題が表示されます。
この様に、コンテストに参加し、コンテストが開始されると問題が表示されます。補足:
コンテストではコンテスト時間内に問題を解いていくことになるのですが
練習用ページなのでコンテスト時間は 2012-06-25(月) 00:00 ~ 2038-01-19(火) 12:14 と、実質無制限になっています。この練習ページでは問題が2つ用意されているようです。
どうやらB問題は初心者向けではないらしいので、A問題だけ解いてみます。
A: Welcome to AtCoder
問題文
高橋君はデータの加工が行いたいです。
整数 $a, b, c$と、文字列 $s$ が与えられます。
$a + b + c$ の計算結果と、文字列 $s$ を並べて表示しなさい。制約
$1 ≤ a, b, c ≤ 1,000$
$1 ≤ | s | ≤ 100$入力
$a$
$b$ $c$
$s$出力
$a + b + c$ と $s$ を空白区切りで 1行に出力せよ。AtCoderWebサイトより引用
解答
問題ではこの様に 問題, 制約, 入力, 出力が与えられます。
また、いつくかの入力例とそれに対する出力例も記載されています。ページを下にスクロールすると、解答提出欄が出てくるので
言語でSwiftを選択します。ここにプログラムを記述し、提出することで問題に解答します。
いろいろな言語が使えるようですね。
また、この問題は練習用なのでいくつかの言語の解答例が乗っています。
(残念ながらSwiftはありません )とりあえず流れの確認ということなので、パパッと解いてしまいましょう。
こんな感じになります。// 標準入力からIntを読み込む let a = Int(readLine()!)! // 標準入力からInt配列を読み込む let bc = readLine()!.split(separator: " ").map { Int($0)! } // 標準入力から文字列を読み込む let s = readLine()! // bcは配列なので、bc[0]にb bc[1]にcが入っている let sum = a + bc[0] + bc[1] // スペース区切りで出力 print(sum, s)これでOK!提出!
と行きたいことろですが...少し心配ですね...
タイプミスをしていてコンパイルエラーになっているかもしれませんし、そもそも間違った解答をしているかもしれません...そんな時は!ページの上のタブから「コードテスト」を開き、テストをしてみましょう。
ソースコードに書いたコードを貼り付け
標準入力に問題のページのサンプル入力をコピーして貼り付け、実行しましょう。サンプル出力と同じ出力が出てきたら成功です。
補足:
実際の提出では、サンプル入力以外にも複数個(10~200程度)の入力でテストされ、全てに正解しなければいけないため、サンプル入力に正解できたからと言って提出が正解になるとは限りませんので注意してください確認もできたので、提出をしましょう。
提出ボタンを押すと、提出結果画面に切り替わるので結果が出るのを待ちます。補足:
なお、もし提出した回答が不正解になっても、何度でも再提出できます。
ペナルティと言う物があるのですが、これは(おそらく)レーティングやパフォーマンスに関係する物なので、最初はあまり気にしなくてもいいかもしれません。「AC」と結果が出ました!これは「正解」という意味です。
補足:
「AC」は Accepted の略で、「解答がテストをパスし、正解と判定された」ということらしいです。
その他の結果は以下を参照してください。
- 用語集: https://atcoder.jp/contests/abc074/glossaryこれで問題を1つ解くことができました!
この「問題文を読む→コーディングをする→(テストをする)→解答する」が問題を解く一つの流れとなっています。
本当のコンテスト、例えば初心者向けのABC(AtCoder Begginer Contest)では100分で6問出題されるので時間内にどんどん問題を解き進めていくことになります!AtCoder Beginners Selection を解く
さて、コンテストの流れもわかったので早速コンテスト!
と、いきたいところですが...まだ少しどんな問題が出るか心配ですし
コンテストは主に土日の21時からやっていることが多いようで、いつでも参加できるわけではないようです。そこで、AtCoder Beginners Selection という初心者用の問題集があるらしいのでやってみましょう!
準備をする
と、その前に...
先ほどはコンテストページで直接コーディングし、テストもしましたが
SwiftだったらXcodeでコーディングしたいし、テストもXcodeでやっちゃいたいです。
なので、コーディング環境の準備をしてから問題に臨みましょう。コーディング&テスト環境の準備
Xcodeを開き、Create a new Xcode Project から macOS の Command Line Tool を選択します
補足:
最初、Playgroundでやろうと試して見たのですが、Playgroundでは標準入力が行えないようなので、普通のProjectでやることにしました。言語がSwiftになっていることを確認し、作成します。
名前と場所はお好みで。作成ができたらいつも見慣れた画面になると思います。
これでコーディングとテストはXcode上で快適に行えるようになりました。
ここでコーディング・テストをしたらAtCoderの提出欄にコピペし、提出すれば良いわけです。また、標準入力/出力は右下のウィンドウで確認できます。
実行してもウィンドウが表示されない場合は右上のボタンをクリックしましょう。
標準入力の受付が始まっても何も表示されないので、「動いてるのかな?」と思っちゃいますが、動いてます。大丈夫です。ウィンドウに入力をしてEnterを押せば動きます。こんな感じでXcodeの準備は完了です!
なお、2020/07/26現在AtCoderのSwiftのバージョンは 5.2.1 であり、これはXcode11.4に対応します2
つまり、Xcode11.4.1以上のバージョンのXcodeを使っている方は提出環境とSwiftのバージョンが異なるため
(メジャーバージョンが変わらない限りほとんどないとは思いますが)
「Xcodeでは実行できたのに提出したらコンパイルエラーになった」なんてこともあり得ます。
心配な方は、練習コンテストでやったようにAtCoderのコードテストページを利用するか、AtCoder用にXcode11.4をダウンロードして利用しましょう。私はMacの容量がカツカツなので心配な時はAtCoderのコードテストページを使うことにします...
- (非公式)Xcodeダウンロードサイト: https://xcodereleases.com/
また、どうやらAtCoderではLinux上でSwiftを動かしているためか(?)使えるライブラリには限りがあります。
Foundation(NS系のクラスやsqrt()など)は動作を確認したのですが、AppKit(UIKitのmacOS版)などはコンパイルエラーになりました。
おそらく、Foundationくらいしか使う機会がないと思うので大丈夫だとは思います...。操作の確認
練習用コンテストではプログラムについてはサラッと流してしまったので
ちょっとだけ競プロに使う基本的な操作を確認しておきましょう。標準入力
標準入力(String)let line = readLine()!
readLine()
は一行をString?型で読み込みます。
オプショナルになっているのでアンラップしましょう。むむ...強制アンラップ...guardしなきゃ...という気持ちは抑えてください。
プログラムが落ちた場合は「問題文の入力形式を読み間違えている」という Logic failure3ですまた、Int型などで扱いたい場合はキャストし、
複数の値が入ってくる場合は配列で取得しましょう。標準入力(Int)let number = Int(readLine()!)! let numbers = readLine()!.split(separator: " ").map { Int($0)! }この辺りの処理はメソッド化して、コピペできる様にしておくと便利です
一行に2値入ってくる場合、一々numbers[0]
,numbers[1]
と指定するのが面倒なのでタプルで取得できるメソッドも追加しちゃいましょう。AtCoderでは複数の値の場合、形式はスペース区切りで統一されているようなので、
split(separator: " ")
でベタ書きしちゃいます。標準入力のテンプレートメソッドたちfunc readInt() -> Int { return Int(readLine()!)! } func readInts() -> [Int] { return readLine()!.split(separator: " ").map { Int($0)! } } func readTwoInts() -> (a: Int, b: Int) { let ints = readLine()!.split(separator: " ").map { Int($0)! } return (a: ints[0], b: ints[1]) } let number = readInt() let numbers = readInts() let (x, y) = readTwoInts()標準出力
標準出力let n = 10 print("YES") // >YES\n print(n, "YES") // >10 YES \n // 引数は何個でも指定可能 print(n, "YES", "NO") // >10 YES NO\n文字列展開を使って
print("\(n) YES")
と書きそうになりましたが...
AtCoderではスペース区切りで出力する形式が多いっぽいので、自動で半角スペースを入れてくれる複数引数の方法が重宝しそうです。ループ
いろいろなループlet numbers = readInts() var sum = 0 for i in 0..<numbers.count { sum += numbers[i] } for number in numbers { sum += number } numbers.forEach { number in sum += number } // 引数名省略 numbers.forEach { sum += $0 }お好きなのをお使いください。
ただ、
.forEach
などの高階関数系はbreak
,continue
等のハンドリングができなかったりしたり、インデックスによる細かい処理がしにくかったりするため
.forEach
などの高階関数系は簡単なループを行う場面で使い、それ以外では普通のfor
を使うのが良いかと思います。ちなみに
.forEach
でインデックスを使いたい場合は.enumrated()
を使いますnumbers.enumrated().forEach { (index, element) in ... }問題を解く
さて...準備が整ったところでいよいよ問題を解いていきましょう。
以下、解答が乗っているので自分の力で解きたい方は閲覧注意です。
第1問: ABC 086 A - Product
問題文
シカのAtCoDeerくんは二つの正整数 $a, b$ を見つけました。$a$ と $b$の積が偶数か奇数か判定してください。制約
$1 ≤ a, b, c ≤ 1,000$
$a,b$ は整数入力
$a$ $b$出力
積が奇数なら'Odd'
と、 偶数なら'Even'
と出力せよ。AtCoderWebサイトより引用
解答
この問題は「A問題」と言われている簡単な部類の問題で、if, switch等の条件分岐ができれば解ける問題になっているそうです。(ループを使わなくても解ける問題になっているらしい)
ABC(AtCoder Beginner Contest) という初心者用のコンテストでは「A, B, C, D, E, F」の6問構成となり、後ろに行くほど難しい問題になる(ことが多い)という構成になっているようで、
配点も「100, 200, 300, 400, 500, 600」となっている(ことが多い)ようです。
問題のID(?)からも ABC086'A' と、どのレベルの問題なのかがわかります。今回は、二整数の積の偶奇を判定するだけなので、愚直に実装します。
func main1() { let (a, b) = readTwoInts() let result = a * b print(result.isMultiple(of: 2) ? "Even" : "Odd") } main1()準備で作成した
readTwoInts()
メソッドを活用していきます。
偶数の判定をresult % 2 == 0
ではなくisMultiple(of: 2)
といった感じで記述できるのは可読性の高い思想を持ったSwiftの良さですね。余談ですが、個人的にSwiftのメソッドの引数のラベルと外部引数名のシステムがとても気に入っていて、
外部引数名をラベルとして利用できることによってメソッド名を冗長にすることなく、表現力の高いメソッドを記述することができ、結果として可読性の高いプログラムを書くことができるためです。
今回の例では、result.isMultiple(of: 2)
はresult is multiple of 2と、自然な英文のようにメソッドをの処理を読むことができます。補足:
Swiftはトップレベルでコードを記述できますが
処理の途中でreturn
を使って一気に処理を終了させたりした場合があるので
「問題ごとにメソッドを定義し、メソッド内に処理を記述してトップレベルでメソッドを実行する」という形でやっていきます。
提出欄にコピペする際に、main1()
をコピペし忘れると何も実行されずに終わって不正解になってしまうので注意してください!第2問: ABC081A - Placing Marbles
問題文
すぬけ君は $1, 2, 3$の番号がついた $3$ つのマスからなるマス目を持っています。 各マスには
'0'
か'1'
が書かれており、マス $i$ には $s_i$が書かれています。すぬけ君は $1$ が書かれたマスにビー玉を置きます。 ビー玉が置かれるマスがいくつあるか求めてください。
制約
$s_1s_2s_3$は'1'
あるいは'0'
入力
$s_1s_2s_3$出力
答えを出力せよ。AtCoderWebサイトより引用
解答
「ビー玉」や「マス」などの単語は出てきていますが、結局のところ 「入力の3つの数字の中に1はいくつありますか」 という問題です。
また、数字が0or1のため、各桁の数字の和を取ってしまえば答えになります。func main2() { let string = readLine()! let count = string.reduce(0) { (sum, part) in sum + Int(String(part))! } print(count) } main2()今回はスペース区切りではなく、
'1'
or'0'
が3つ連続して入ってくるためString型で受け取り、処理します。
愚直にfor文を用いて記述してもよかったのですが、せっかくなのでreduce
を使って「1文字を取得→Intにキャスト→加算」という方法で実装して見ました。補足:
String型はSeaquence
プロトコルに準拠しているため、map
,filter
,reduce
等を用いて1文字づつ処理ができます。第3問: ABC081B - Shift only
問題文
黒板に $N$ 個の正の整数 $A_1,...,A_N$ が書かれています.すぬけ君は,黒板に書かれている整数がすべて偶数であるとき,次の操作を行うことができます.
黒板に書かれている整数すべてを,$2$ で割ったものに置き換える. すぬけ君は最大で何回操作を行うことができるかを求めてください.
制約
$1 ≤ N ≤ 200$
$1 ≤ A_i ≤ 10^9$入力
$N$
$A_1$ $A_2$ $...$ $A_N$出力
すぬけ君は最大で何回操作を行うことができるかを出力せよ.AtCoderWebサイトより引用
解答
これはB問題です。
A問題よりかは少し複雑になってはいますが、B問題は 普段アプリケーション開発等のプログラミングをしていれば、競技プログラミングの知識・対策ゼロでも解けるレベル くらいなんじゃないかなと思っています。
また、アプリケーション開発等をしたことがない方にとっても、プログラミング言語自体を学ぶ上でいずれは通るレベル (プログラミング言語の入門書や学習サイトの演習問題に乗っていてもおかしくないような問題)だと思います。今回の問題は、基本的には問題文通りに 「『入力の配列要素が全て偶数であれば全ての配列要素を2で割る』という動作を繰り返し、何回行えたかを数える」 という処理を書いていけばいけます。
func main3() { let N = readInt() var numbers = readInts() var count = 0 while numbers.allSatisfy({ $0.isMultiple(of: 2) }) { count += 1 numbers = numbers.map { $0 / 2 } } print(count) } main3()Swiftの
Seaquence
にはallSatisfy()
という「全ての要素が引数のクロージャーの条件を満たすかどうか」を調べる便利なメソッドがあるので、利用します。
$0
と言うのはクロージャーでの引数名を定義せず番号で指定する方法です。
全ての配列要素を2で割る処理も、map()
を利用します。かなり綺麗に記述できているんじゃないかと思います。やっぱりSwiftは最高だぜ!
競技プログラミングにおいては、プログラミングではなくアルゴリズムに集中したいので、余分な記述が少なく、可読性が高い言語を使うのは結構理に適ったことなんじゃないかなと思います。第4問: ABC087B - Coins
問題文
あなたは、 $500$ 円玉を $A$ 枚、$100$ 円玉を $B$ 枚、$50$ 円玉を $C$ 枚持っています。 これらの硬貨の中から何枚かを選び、合計金額をちょうど $X$ 円にする方法は何通りありますか。同じ種類の硬貨どうしは区別できません。$2$ 通りの硬貨の選び方は、ある種類の硬貨についてその硬貨を選ぶ枚数が異なるとき区別されます。
制約
$0 ≤ A, B, C ≤ 50$
$A + B + C ≥ 1$
$50 ≤ X ≤ 20,000$
$A, B, C$は整数である
$X$は$50$の倍数である入力
$A$
$B$
$C$
$X$出力
硬貨を選ぶ方法の個数を出力せよ。AtCoderWebサイトより引用
解答
ちょっと条件とかが複雑になってきましたね...
どう解けばいいんだ...?賢い解き方があるのか...?と考えていたのですが
これは深読みし過ぎず、普通に全探索で「ありうる全てのA, B, Cの組み合わせを試し、合計金額がX円になる組み合わせがいくつあるか数え上げる」という方法で良いようです。
愚直に3重for文を書きました。func main4() { let A = readInt() let B = readInt() let C = readInt() let X = readInt() var count = 0 for i in 0...A { for j in 0...B { for k in 0...C { if X == (500 * i) + (100 * j) + (50 * k) { count += 1 } } } } print(count) } main4()第5問: ABC083B - Some Sums
問題文
$1$ 以上 $N$ 以下の整数のうち、$10$ 進法での各桁の和が $A$ 以上 $B$ 以下であるものの総和を求めてください。制約
$1 ≤ N ≤ 10^4$
$1 ≤ A ≤ B ≤ 36$
入力は全て整数である入力
$N$ $A$ $B$出力
$1$ 以上 $N$ 以下の整数のうち、$10$ 進法での各桁の和が $A$ 以上 $B$ 以下であるものの総和を出力せよ。AtCoderWebサイトより引用
解答
こちらも問題文通りに愚直に実装します。
func readThreeInts() -> (a: Int, b: Int, c: Int) { let ints = readLine()!.split(separator: " ").map { Int($0)! } return (a: ints[0], b: ints[1], c: ints[2]) } func main5() { let (N, A, B) = readThreeInts() var sum = 0 for i in 1...N { let result = String(i).reduce(0) { (sum, part) in sum + Int(String(part))! } if A <= result, result <= B { sum += i } } print(sum) } main5()3変数の入力が出てきたので
readThreeInts()
を追加です。第2問で出てきたStringの
reduce
処理がまた出てきました。
「各桁の和を算出する」はよくある処理なのでしょうか...?余談ですが...SwiftのifでANDは
,
と&&
のどちらでも良く、個人的にはタイピングが楽なので,
の方が好きなのですが、&&
の方が「アンド」という意味をそのまま表現しているし、「,」じゃ知らない人にとっては意味が曖昧なのでif-let
でもない普通の条件文で用いるのは可読性的にはグレーかもしれませんね...第6問: ABC088B - Card Game for Two
問題文
$N$ 枚のカードがあります. $i$ 枚目のカードには, $a_i$ という数が書かれています.
Alice と Bob は, これらのカードを使ってゲームを行います. ゲームでは, Alice と Bob が交互に $1$ 枚ずつカードを取っていきます. Alice が先にカードを取ります.
$2$ 人がすべてのカードを取ったときゲームは終了し, 取ったカードの数の合計がその人の得点になります. $2$ 人とも自分の得点を最大化するように最適な戦略を取った時, Alice は Bob より何点多く取るか求めてください.制約
Nは$1$以上$100$以下の整数
$a_i$($1 ≤ i ≤ 100$)は$1$以上$100$以下の整数入力
$N$
$a_1$ $a_2$ $...$ $a_N$出力
両者が最適な戦略を取った時, Alice は Bob より何点多く取るかを出力してください.AtCoderWebサイトより引用
解答
「両者が最適な戦略を取った時」というのがキーワードです。
まずAliceが一番目に値が大きいカードを取り、Bobが二番目に値が大きいカードをとり、Aliceが三番目に値が大きいカードを取り...と行動するのでカードをカードの値で降順ソートをしてしまえば
Aliceはインデックス0, 2, 4, 6...
Bobはインデックス1, 3, 5, 7...
と取っていくことになります。つまり、「入力の配列を降順にソートして、偶数番目のインデックスの値をAliceの得点, 奇数番目のインデックスの値をBobの得点とする」 という処理を書けばいいわけです。
func main6() { let N = readInt() let numbers = readInts() var alice = 0 var bob = 0 numbers .sorted(by: >) .enumerated() .forEach { (index, element) in if index.isMultiple(of: 2) { alice += element } else { bob += element } } print(alice - bob) } main6()
sorted()
はデフォルトだと昇順ソートなので、sorted(by: >)
で降順ソートします。配列の先頭の方が大きくなるように...という意味で>
を引数に指定します
for文を使ってもよかったのですが、sorted()
の勢いで高階関数を使って書いてしまいましょう。
インデックスを使いたいのでenumrated()
をしてからforEach()
します。ここのif文とか良いですね。
if index is multiple of 2 ... else ...と、自然に読めてしまいます。
今考えると、Swiftのif, whileなどの条件に()
が必要ないのは、「if文自体をより自然な英文のように読みやすくするため」という意味もあるんじゃないかなと思いました。第7問: ABC085B - Kagami Mochi
問題文
$X$ 段重ねの鏡餅 $(X ≥ 1)$ とは、$X$ 枚の円形の餅を縦に積み重ねたものであって、どの餅もその真下の餅より直径が小さい(一番下の餅を除く)もののことです。例えば、直径 $10$、$8$、$6$センチメートルの餅をこの順に下から積み重ねると $3$ 段重ねの鏡餅になり、餅を一枚だけ置くと $1$ 段重ねの鏡餅になります。ダックスフンドのルンルンは $N$ 枚の円形の餅を持っていて、そのうち $i$ 枚目の餅の直径は $d_i$ センチメートルです。これらの餅のうち一部または全部を使って鏡餅を作るとき、最大で何段重ねの鏡餅を作ることができるでしょうか。
制約
$1 ≤ N ≤ 100$
$1 ≤ d_i ≤ 100$
入力値は全て整数である。入力
$N$
$d_1$
$.$
$.$
$d_N$出力
作ることのできる鏡餅の最大の段数を出力せよ。AtCoderWebサイトより引用
解答
一見難しそうだったのですが...サンプル入力&出力を見ていると
「あれ?これって結局はダブっていない要素の数を数え上げるだけかな...?」
と思いつき、やって見たら正解でした。func main7() { let N = readInt() let numbers = (0..<N).map { _ in readInt() } print(Set(numbers).count) } main7()今までと違う点で言えば一行に1文字づつ入力されてくる点でしょうか。
こう言う場合、個人的には(0..<N).map { _ in readInt() }
という書き方が簡潔かつ入力配列をlet
にできるので気に入って使っています。ダブっていない(=ユニークな)要素を数える方法は、愚直に「記録用の配列を作って新しい要素が来たら追加」という方法でもよかったのですが
ちょうどユニークな要素だけを保持するSet
があるのでこれを使いましょう。
Set
を入力のnumbers配列で初期化し、count
で要素数を数えるだけでOKです。
Dictionary
でも実装できます。「ユニークな要素」と聞いた時に、Set
より先にDictionary
が思い浮かんだ方も多いんじゃないでしょうか。(私はそうでした)func main7() { let N = readInt() var dictionary: [Int: String] = [:] (0..<N).forEach { _ in let number = readInt() dictionary[number] = "found!" } print(dictionary.count) } main7()第8問: ABC085C - Otoshidama
問題文
日本でよく使われる紙幣は、$10000$ 円札、$5000$ 円札、$1000$ 円札です。以下、「お札」とはこれらのみを指します。青橋くんが言うには、彼が祖父から受け取ったお年玉袋にはお札が $N$ 枚入っていて、合計で $Y$ 円だったそうですが、嘘かもしれません。このような状況がありうるか判定し、ありうる場合はお年玉袋の中身の候補を一つ見つけてください。なお、彼の祖父は十分裕福であり、お年玉袋は十分大きかったものとします。
制約
$1 ≤ N ≤ 2000$
$1000 ≤ Y ≤ 2×10^7$
$N$は整数である。
$Y$は$1000$の倍数である。入力
$N$ $Y$出力
$N$ 枚のお札の合計金額が $Y$ 円となることがありえない場合は、'-1 -1 -1'
と出力せよ。$N$ 枚のお札の合計金額が $Y$ 円となることがありうる場合は、そのような $N$ 枚のお札の組み合わせの一例を「$10000$ 円札 $x$ 枚、$5000$ 円札 $y$ 枚、$1000$ 円札 $z$ 枚」として、$x$、$y$、$z$ を空白で区切って出力せよ。複数の可能性が考えられるときは、そのうちどれを出力してもよい。
AtCoderWebサイトより引用
解答
C問題です。C問題は 愚直には解けず、問題の読み替えや少しの慣れとテクニックが必要なレベル だと感じています。
C問題からは愚直に解くと計算時間やメモリの使用量を制限を超過し、不正解となる可能性が出てくるので、プログラムのアルゴリズムを工夫する必要が出てきます。今回の問題を愚直に解こうとすると、第4問のように全探索で 「$i + j + k = N$となる全ての$i$, $j$, $k$の組み合わせを試し、合計金額がY円となる組み合わせを見つける」 となりますが、
実はこれではTLE(実行時間制限超過)で不正解になってしまいます。3重ループを行おうとすると以下のような記述になり、制約を見ると$1 ≤ N ≤ 2000$とあることから、全ての組み合わせの判定を行うためには
$2,001$通り $×$ $2,001$通り $×$ $2,001$通り = $8,012,006,001$通り $≒$ $8 × 10^9$通り
の判定が必要となります。
一般的な計算機が1秒間に実行できる計算量は大体$10^8$程度であり、$8 × 10^9$通りの判定は桁が一つ大きいので間に合いません。for i in 0...N { for j in 0...N { for k in 0...N { // 判定処理 } } }補足:
計算量を考える場合に用いられるオーダー($O$)という物があります。
「この処理の計算量は$O(n)$」のような形で用いられ、「nに比例する時間以内で終了する計算量」という意味です。
今回の例で言えば、愚直に解くと3重ループとなるので、$O(n^3)$となり、$n=2000$となります。
$n^3$ > $10^8$ であることがわかるので、「$O(n^3)$=(nについての3重ループ)ではこの問題は解けない、$O(n^2)$=(nについての2重ループ)で解かなくては...」と考えるわけです。が...計算量の概念はとても重要だとは思いますが、少し慣れるのに時間がかかる(と思うので)、個人的には最初は「3重ループで解けそう!あ...実行時間制限超過が出た...2重ループに修正しよう!」とトライ&エラーをしていくうちに計算量のことも考えられるようになればいいのではないか、と思っています
...という自分の勉強方針でした(まる長々とお話をしてしまいましたが、解答はこんな感じです。
func main8() { let (N, Y) = readTwoInts() for i in 0...N { for j in 0...N { let k = N - i - j if k >= 0, Y == (i * 10000) + (j * 5000) + (k * 1000) { print(i, j, k) return } } } print(-1, -1, -1) } main8()$i + j + k = N$から、$i$, $j$を決めた時点で$k$の値は$k = N - i - j$と一意に決まるので、kについてループする必要はありません。これで計算量を$O(n^2)$に減らすことができました。
後は$i$, $j$, $k$を用いて判定を行えば良いのですが、kが負になる場合を弾くのと、答えが1つ見つかったらプログラムを終了させるのだけ忘れないようにします。また、
func main8()
とメソッド化しているおかげで多重ループからの離脱もreturn
で楽にできるようになっています。第9問: ABC049C - 白昼夢
問題文
英小文字からなる文字列 $S$ が与えられます。 $T$ が空文字列である状態から始め、以下の操作を好きな回数繰り返すことで $S = T$ とすることができるか判定してください。
- $T$ の末尾に
'dream'
'dreamer'
'erase'
'eraser'
のいずれかを追加する。制約
$1 ≤ |S| ≤ 10^5$
Sは英小文字からなる。入力
$S$出力
$S = T$ とすることができる場合'YES'
を、そうでない場合'NO'
を出力せよ。AtCoderWebサイトより引用
解答
うーん...悩みましたが初見では解けませんでした。
「文字列Sは先頭から読むと一意に定まらないが、後ろから読むと一意に定まる」というヒントを聞いてやっと解くことができました。C問題以上では発想力も求められそうですね...
まあでも、こういう手法をテクニックとして消化し、引き出しを増やしていけば簡単に気付けるようになるのかもしれません。func main9() { let words = ["dream", "dreamer", "erase", "eraser"] var S = readLine()! while let word = words.first(where: { S.hasSuffix($0) }) { S.removeLast(word.count) } print(S.isEmpty ? "YES" : "NO") } main9()
while-let
,first()
,hasSuffix()
を用いて
「『wordsのどれかに後方一致したら一致した部分を削除する』という操作を可能な限り繰り返し、繰り返しが終わった時点で全ての文字列が削除できていたらYES」
というアプローチです。補足:
本当はもう少し長いコードだったのですが、綺麗な解答をQiitaの記事で見つけたので参考にさせていただきました。先人様:
https://qiita.com/conf8o/items/8f2130510a9deaffa312#abc049c---%E7%99%BD%E6%98%BC%E5%A4%A2今回は計算量は大丈夫でしたが、一点今後注意すべき点があります。
それは、SwiftにおけるStringの扱いです。Swiftの
Collection
にはcount
というコレクションの長さを取得する変数があります。
そこで問題なのが、Stringのcount
は $O(1)$ではなく$O(n)$の計算量がかかります
k番目の文字の取得にもO(k)の計算量がかかりますドキュメントには以下のような記述があります。
O(1) if the collection conforms to RandomAccessCollection; otherwise, O(n), where n is the length of the collection.
Collection count
公式ドキュメント:
https://developer.apple.com/documentation/swift/collection/3017670-countコレクションが
RandomAccessCollection
に準拠している場合は$O(1)$, そうでない場合は$O(n)$となるようです。
次に、RandomAccessCollection
のドキュメントを見てみると...
RandomAccessCollection
公式ドキュメント: https://developer.apple.com/documentation/swift/randomaccesscollectionな、なんと
Conforming Types
にStringが無いではありませんか...!!
つまり、Stringのcount()
は$O(n)$かかるということを意味します。C言語, C++, Java, Pythonなどを触ってきた自分は「String=Charの配列」であることが当たり前かのように考えていたのですが、Swiftではそうでは無いようです...。4
例えば、for文ループの中でcountや特定の文字取得をしなければいけなく
for文 * count = $O(n^2)$ * $O(n)$ = $O(n^3)$になってしまう...
なんて場合の解決策としては、NSString
を使うもしくは、[Character]
にキャストして扱う ことです。
(ただし、NSString
,[Character]
の変換にコピーが走るので$O(n)$かかります。)
ループの外でNSString
もしくは[Character]
へ変換をしておくことで、
変換 + for文 * count = $O(n)$ + $O(n^2)$ * $O(1)$ = $O(n^2)$ とすることができます。
実際にそんなストライクな問題が出題されるかは謎ではありますが...let string = "0123456789" string.count // O(n) let nsString = NSString(string) // キャストでO(n) nsString.length // O(1) let arrayOfChar: [Character] = Array(string) // キャストでO(n) arrayOfChar.count // O(1)また、Stringのk番目の文字を取得する際も
string[k]
とはできず、String.Index
を用いてstring[string.index(string.startIndex, offsetBy: k)]
と書かなくてはいけません。
競技プログラミングでこれをするのはちょっと面倒な気はしますね...補足:
subscript
(string[k]
とかの指定方法)でIntを使って簡単にできないようにしているのは、
あえて複雑な処理にしておくことで高コストな処理であることを意識してもらうため 5 だそうです。
確かにそう言われると、string.index(string.startIndex, offsetBy: k)
という表現から
インデックスの先頭(string.startIndex
)からk番ずらした(offsetBy
)インデックスを取得して...と、「$O(k)$かかってる感」が出てる気がします(???)これだけ書いておいてなんですが、今回の例のように
hasSuffix()
やremoveLast()
などのメソッドで事足りる場合も多いと思うので、[Character]
への変換などは本当に迫られた時だけにしようと思いました。
「Stringの扱いには少し注意が必要」と言う程度に覚えておくことにします。本題からかなりそれてしまいましたが...
おかげでSwiftのStringの仕様を少しだけ理解できたような気がします!
計算量について知れたのはとても良かったです。第10問: ABC086C - Traveling
問題文
シカのAtCoDeerくんは二次元平面上で旅行をしようとしています。 AtCoDeerくんの旅行プランでは、時刻 $0$ に 点 $(0, 0)$ を出発し、 $1$ 以上 $N$ 以下の各 $i$ に対し、時刻 $t_i$ に 点 $(x_i, y_i)$ を訪れる予定です。AtCoDeerくんが時刻 $t$ に 点 $(x, y)$ にいる時、 時刻 $t + 1$ には 点 $(x + 1, y), (x − 1, y), (x, y + 1), (x, y − 1)$ のうちいずれかに存在することができます。 その場にとどまることは出来ないことに注意してください。 AtCoDeerくんの旅行プランが実行可能かどうか判定してください。
制約
$1 ≤ N ≤ 10^5$
$0 ≤ x_i ≤ 10^5$
$0 ≤ y_i ≤ 10^5$
$1 ≤ t_i ≤ 10^5$
$t_i ≤ t_{i+1}(1≤i≤N - 1)$
入力は全て整数入力
$N$
$t_1$ $x_1$ $y_1$
$t_2$ $x_2$ $y_2$
$.$
$.$
$t_N$ $x_N$ $y_N$出力
旅行プランが実行可能ならYesを、不可能ならNoを出力してください。AtCoderWebサイトより引用
解答
最後の問題です。
まず、問題を小さく分割します。「プランの時刻$t_i$から時刻$t_{i+1}$までについて可能かどうかを判別する」を最初から最後まで計N-1回行えば良いとわかるので、時刻$t_i$から時刻$t_{i+1}$までのことのみを考え、あとはループさせれば良いことがわかります。
次に、分割した問題について考えます。
プランの時刻$t_i$から時刻$t_{i+1}$までについて
- 距離が時間より大きい場合はたどり着けない: $|x_i - x_{i+1}| + |y_i - y_{i+1}|$ $>$ $t_{i+1} - t_i$
- 偶数時間は「どこか隣の位置に移動し、戻ってくる」という移動を繰り返すことで消費することができる(偶数時間は結果的に位置を変えずに潰すことができる)
- 目標の点までの移動の仕方で残り時間の偶奇が変わることはない(なんとなく直感的にわかるとは思います)
という点から、「時間以内に(最短距離で)たどり着くことができ、また辿り着いた後、残り時間が偶数時間であれば良い」ということがわかります。
計算量についても、プランの時刻$t_i$から時刻$t_{i+1}$までの判定については$O(1)$で解けるので、それをループした$O(N)$で済みそうです。$N≤10^5$なので計算時間は余裕ですね。
よって以下のように実装できます。
typealias Plan = (t: Int, x: Int, y: Int) func main10() { let N = readInt() let plans: [Plan] = (0..<N).map { _ in readThreeInts() } var previous = Plan(t: 0, x: 0, y: 0) for plan in plans { let time = plan.t - previous.t let distance = abs(previous.x - plan.x) + abs(previous.y - plan.y) // 最短距離で目標地点まで移動した場合の残り時間=時間-距離 let remain = time - distance // 残り時間が負 かつ 偶数 だったらOK if remain < 0 || !remain.isMultiple(of: 2) { print("No") return } previous = plan } print("Yes") } main10()$(t, x. y)$のペアの保持はタプルを使い、可読性向上のため
typealias
を使ってPlan
として定義しています。
プランはPlan
型配列として受け取り、その配列の$i$番目と$i+1$番目について判定を行います。プログラム的にはそこまで難しくないと思います。
競技プログラミングは(発想力と)アルゴリズム力を競う物らしいので、問題が難しくなってもプログラム自体はそこまで難しくならない、と言った感じなのでしょうか...。あと、問9, 10をみてわかるかと思いますが、YES/NO問題は問題によって
'Yes'
だったり'YES'
だったりします...。問題文を良く読まないと変なところではまる場合があるので注意しましょう(経験談)(戒め)最初、タプルではなく
struct
を用いて実装を行っていました。
競技プログラミングにおいてstruct
(やクラスなど)は問題を解く上で一般的に使われているのでしょうか...?
「Swiftで可読性の低いプログラムを書きたくない」と言う気持ちがあるので、構造化できる物があったら構造化したくなってしまいます...struct Plan { let time: Int let x: Int let y: Int func diff(to plan: Plan) -> (time: Int, distance: Int) { return (plan.time - time, abs(x - plan.x) + abs(y - plan.y)) } } func main() { let N = readInt() let plans = (0..<N).map { _ -> Plan in let tuple = readThreeInts() return Plan(time: tuple.a, x: tuple.b, y: tuple.c) } var previous = Plan(time: 0, x: 0, y: 0) for plan in plans { let diff = previous.diff(to: plan) let remain = diff.time - diff.distance if remain < 0 || !remain.isMultiple(of: 2) { print("No") return } previous = plan } print("Yes") }これで10問解けました!
問題から題意を読み解く点, 解法が閃いたときの嬉しさなど、数学に似ている気がしますね...。
後半はもっぱらキーボードではなくiPadのメモ帳と向き合いながら考えていたので、プログラミングと言うより数学をしている感覚でした。コンテストに参加する
よし!!初心者向け問題集も解いた!!さぁコンテストだ!!
以下のページから開催予定のコンテストを確認してみましょう!
- AtCoderホーム: https://atcoder.jp/home
基本的にまずは初心者向けのコンテストである ABC(AtCoder Begginer Contest) に参加すれば良いと思います。
ABCは土または日の21時ごろから始まる場合が多く、また、開催周期は不定期のようです。こちらも下の記事が詳しいと思います。
- AtCoder コンテストについての tips: https://qiita.com/drken/items/8a6f139158cde8a61dce
過去問を解く
コンテストがあった場合もなかった場合も、空いた時間は過去問を解くと良いと思います!
私は下の AtCoderProblems と言う過去問へのリンクが乗っているサイトを利用しています。
- AtCoderProblems: https://kenkoooo.com/atcoder/#/table/
「過去のC問題を一通り解いてみる」みたいな使い方ができたり、
レーティングに沿って問題の難易度が色分けされているので、「今自分がどの程度の難易度(レーティング)の問題まで解けるのが」などが分かったりして便利です。まとめ・感想
今回、AtCoderをSwiftで初めてみた体験記と、Swiftを使っている方々が競技プログラミングを始めやすいように解説を兼ねて記事を書きました。
いかがだったでしょうか?Swiftや競技プログラミングに興味を持っていただけたでしょうか?普段メインでは扱わないアルゴリズムを、普段使っているSwiftで書くことによって余分な記述が少なく、可読性が高くて描きやすいSwiftの良さを再確認できたと同時に、今まで見えていなかったSwiftの世界が見えた気がします。また、新しく得た知見は普段Swiftをアプリケーション開発で書く場合にも十分に役に立つと思うのでとても得をした気分です。
せっかく始めたので、これからも地道に競技プログラミングを続けていこうと思っています。本当は、もう少し競技プログラミング・AtCoderについて学んでから記事を書こうと思っていたのですが、Swiftで競技プログラミングをやる仲間を増やしたい&早い方がいいだろう、と言うのと初心者なりに得た情報をアウトプットすることで勉強になると思ったので書くことにしました。
しかし、「でもやっぱり私も1ヶ月前くらいに競技プログラミングを始めたばかりで人に解説できるほど競技プログラミングを知らないからな...」と言う気持ちと「Swiftを使っている方々で競技プログラミングに興味のある方の参考になる記事を書きたいな」と言う気持ちが鬩ぎ合って, 結果説明口調になったり感想になったりと、解説なのか体験記なのか良くわからないどっちつかずの記事になってしまいました...もしここまで読んでくださった方がいらっしゃったら、本当にありがとうございます。
だいぶ長い記事になってしまったので、ここら辺で終わりにしたいと思います。それでは、良き競技プログラミング, Swiftライフを。
次はコンテストでお会いしましょう!
【ニュースリリース】日本最大のプログラミングコンテストサイトAtCoder登録者数が20万人を突破! https://twitter.com/atcoder/status/1269882731313258496?s=20 ↩
XcodeとSwiftのバージョン対応表: https://qiita.com/uhooi/items/b16c0959aa6e3caf5426 ↩
Swiftのエラー4分類が素晴らしすぎるのでみんなに知ってほしい
https://qiita.com/koher/items/a7a12e7e18d2bb7d8c77#logic-failure ↩なぜSwiftの文字列APIは難しいのか https://postd.cc/why-is-swifts-string-api-so-hard/ ↩
Swift 3のStringのViewに対して、Intでsubscript出来ない理由 https://medium.com/swift-column/swift-string-7147f3f496b1 ↩
- 投稿日:2020-07-29T10:32:13+09:00
テストを行う。
対象読者
・swiftを始めたばかりな方
・Testをまともに書いたことのない方
・Testって何?な方
・swiftすきな方Testの大切さ
まず、動作確認(Test)はとても大切です。
作ったアプリがきちんと動くか、実行して自分で触って予定通りの動作を確認する。これはとても大切でリリースした後にバグ祭りなんて自体を少なからず避けられます。
Testは何のためにするのか?
Testをするのは
・動作の確認の為
・動作を保証する為
・変更に耐えられる様にする為これらが挙げられます。
その為、これが保証されれば、「自分で実行させて触って動作を確認する」というTestでもいいのです。手動Testは現実的か?
実際、開発が進むたびに手動でテストするのはとても気が遠くなります。
アップデートを施せば、手動テストで通っていた場所も当然の様にバグが顔を出します。(コンニチハ!!!)
Testを書くのはめんどくさい。
Test書いたことない頃は「Test書くのめんどくせえ」と筆者は思っていました。
というか正直Testを書いている今でもめんどくさいです。
ただ、開発スピードを考えても自動でテストを行うほうが速いし、長期的にはかなり楽になります。
簡単なTestを書いてみよう。
プロジェクトを作る際にXCTestとXCUITestを追加しておきます。
(後から追加できます。)単体テスト
単体テスト(同期)
テスト対象は以下の様に、よくあるただ足し算をする関数。
SyncModel.swiftimport Foundation final class SyncModel { func add(_ x: Int, _ y: Int) -> Int { return x + y } }そして以下がそのテストコード。
XCTestfunc testSyncAdd() throws { let x = 10 let y = 5 let ans = syncModel.add(x, y) XCTAssertEqual(ans, 15) }XCTAssertEqualは二つの引数を比較して一致しなければエラーを投げます。
単体テスト(非同期)
次に非同期のテスト。
closureに計算結果を渡すものです。AsyncModelimport Foundation final class AsyncModel { func add(_ x: Int, _ y: Int, completion: @escaping (Int) -> Void) { DispatchQueue.global().asyncAfter(deadline: .now() + 3) { completion(x + y) } } }そして以下が対象のテストコード
XCTestfunc testAsyncAdd() throws { let exp = expectation(description: "add function") let x = 10 let y = 5 asyncModel.add(x, y) { ans in XCTAssertEqual(ans, 15) exp.fulfill() } wait(for: [exp], timeout: 5) }expectationを使って非同期処理のテストを行ってます。
wait関数を記述しなければ非同期処理を待たずにこのテスト関数は終了し、正しいテストはできません。
ではwaitは何を待っているのでしょうか?
それは引数forで指定したexpがfulfillを投げるのを待っています。非同期処理が終わったタイミングでexpがfulfillを読んでいるのが確認できますね。
これでwait以下が実行されるので段階的に実行することができるというわけですね。UITest
例として遷移のテストを行います。
コードはここに書くには少し鬱陶しいので簡単に説明します。「ボタンを押したら別のViewControllerに遷移するアプリ」です!!!
これに対するテストは
XCUITestfunc testPush() throws { let app = XCUIApplication() app.launch() let pushButton = app.buttons["pushButton"] XCTAssert(pushButton.exists) pushButton.tap() let backButton = app.buttons["Back"] XCTAssert(backButton.waitForExistence(timeout: 5)) backButton.tap() let pushButton2 = app.buttons["pushButton"] XCTAssert(pushButton2.waitForExistence(timeout: 5)) }まず、app.launchでアプリを起動します。
次に遷移するためのボタンを取得します。
これは、UIの宣言側でViewControllerbutton.accesibilityIdentifier = "pushButton"などと設定することでTest側でUIを特定できる様にします。
Test側はapp.buttons["pushButton"]
で取得できるのですからよくできています。(ウンウン)プッシュされた先のViewControllerでは設定していなければ左上のナビゲーションアイテムとしてBackボタンが設定されているはずです。
そのため、
XCUITestlet backButton = app.buttons["Back"] XCTAssert(backButton.waitForExistence(timeout: 5))この様にBackボタンを取得、存在を確認することでテストを行います。
結合テスト
単体テストでやった単純な足し算関数とUIテストを組み合わせてテストします。
テスト対象は「足し算の入力を設定する2つのテキストフィールドとボタンを持ち、遷移先には足し算の結果を出力するラベルを持つアプリ」です!!!
これに対するテストコードは以下の様になります。
XCUITestfunc testAdd() throws { let app = XCUIApplication() app.launch() let xField = app.textFields["xField"] let yField = app.textFields["yField"] XCTAssert(xField.exists) XCTAssert(yField.exists) let pushButton = app.buttons["pushButton"] XCTAssert(pushButton.exists) xField.tap() xField.typeText("10") yField.tap() yField.typeText("5") pushButton.tap() let backButton = app.buttons["Back"] XCTAssert(backButton.waitForExistence(timeout: 5)) let ansLabel = app.staticTexts["ansLabel"] XCTAssert(ansLabel.exists) XCTAssertEqual(Int(ansLabel.label), 15) }例によってテキストフィールドはaccesibilityIdentifierにxField, yFieldを指定し、取得しています。
取得したUIにtapでフォーカス、typeTextで文字を入力してもらう動作をしていますね。
xField.tap() xField.typeText("10")続いて一つ前のUITestでやっていた様に遷移します。
遷移はBackボタンが取得できているかで完了確認を簡易的に行っています。let backButton = app.buttons["Back"] XCTAssert(backButton.waitForExistence(timeout: 5))textFieldsやbuttonsなどのUIはそのままの名前だったので迷わず取得できますが、
ラベルはstaticTextsから取得します。紛らわしいですね!ラベルのテキストはlabelプロパティで取得します。
値がしっかり反映されているかを確認してテストを終えます。
XCTAssertEqual(Int(ansLabel.label), 15)如何だったでしょうか?
テストはとても大事です。(「「「2回目」」」)
ただ、今までテストを書いたことがない人がいきなり完璧なテストを網羅的に書くのも、現実的ではありません。
まずは一つ、簡単なテストを書いてみては如何でしょうか?
ご愛読ありがとうございました。(完)
- 投稿日:2020-07-29T01:49:58+09:00
Swift:NSImageからDataを生成するときのポイント
背景
Twitterに画像を投稿するために、画像のファイルURLから
NSImage
を生成して、それをData
に変換してPOST APIに投げるというコードを書いていました。一見上手く動いているように見えて、特定のPNG画像だと色がおかしい状態で投稿されることがあることが判明し、原因を究明したところ、NSImage
からData
を生成する時、注意しないとデータの透明度に関するデータが欠損するのか、不完全なData
が生成されることがわかりました。対処方法
検索して出てきた記事によると、いったん
NSImage
をCGImage
に変換してからData
にすると良さそうだと判明したのでそちらを使ったところ上手く行きました。実験に使ったサンプルコード
Command_Line_Tool_Projectimport Foundation import AppKit func saveImage(name: String, data: Data) { guard let dir = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask).first else { exit(3) } let url: URL = dir.appendingPathComponent(name) do { try data.write(to: url) } catch { Swift.print(error.localizedDescription) } } // 良い方法 func goodMethod(image: NSImage) -> Data? { guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return nil } let rep = NSBitmapImageRep(cgImage: cgImage) return rep.representation(using: .png, properties: [:]) } // 悪い方法 func badMethod(image: NSImage) -> Data? { guard let rep = image.representations.first as? NSBitmapImageRep else { return nil } return rep.representation(using: .png, properties: [:]) } guard let image = NSImage(contentsOfFile: "/Users/ユーザ名/Desktop/sample.png") else { exit(0) } if let data = goodMethod(image: image) { saveImage(name: "good_image.png", data: data) } if let data = badMethod(image: image) { saveImage(name: "bad_image.png", data: data) }
- 投稿日:2020-07-29T00:18:11+09:00
たった14行でiOSアプリ内のリンクをリッチに表示する方法
FacebookやTwitterでURLを投稿すると、リンク先のタイトルや画像を自動で取得してカードのようなUIとして表示されるのを見たことがあるでしょうか。そんな機能を簡単に実装できるApple標準のフレームワークがXcode11以降で使えるようになりました。
例えばYoutubeのリンク
Youtube動画の場合、リンクをアプリに埋め込むとそのまま再生できました。
LinkPresentationフレームワークを使うと、Youtube動画とかアプリ内でそのまま再生できるのね。https://t.co/iMYuqb2zaF pic.twitter.com/FxmEF6ygrA
— Natsui@?iOSエンジニア (@MeSummery) July 28, 2020LinkPresentation framework
URL からメタデータを取得し、アプリ内でリッチなリンクコンテンツをいい感じに表示してくれるAppleのフレームワーク。iOS 13.0以降から利用可能。
https://developer.apple.com/documentation/linkpresentation実装ステップ
利用箇所でまずフレームワークをインポートしておきましょう。
import LinkPresentationURLからメタデータを取得する
startFetchingMetadata(for:completionHandler:)
をコールしてメタデータの取得を開始します。let metadataProvider = LPMetadataProvider() let url = URL(string: "https://youtu.be/hMgcaYd2BZM")! // リッチに表示したいURL metadataProvider.startFetchingMetadata( for: url, completionHandler: { metadata, error in // Make use of fetched metadata. })
completionHandler
はバックグラウンドキューで実行されます。
クロージャー内でUIを操作する場合には、メインスレッドで行うようにしましょう。メタデータを専用Viewに受け渡す
LPMetadataProvider
から受け取ったメタデータをLPLinkView
の初期化時に渡します。metadataProvider.startFetchingMetadata( for: url, completionHandler: { metadata, error in if error != nil { return } guard let metadata = metadata else { return } DispatchQueue.main.async { // メインディスパッチキューで実行 let linkView = LPLinkView(metadata: metadata) // メタデータを渡す self.previewView.addSubview(linkView) // Storyboard上に配置した任意のUIView // Sizing } })今回は、生成した
LPLinkView
のインスタンスをStoryboard上に配置したpreviewView
にaddSubview
しました。
また、前述のとおり、completionHandler
内のUIの更新は、DispatchQueue.main
で実行しましょう。サイズを決定する
サイズを指定したら、
sizeToFit()
で任意のサイズにレイアウトを合わせましょう。linkView.frame.size = self.previewView.frame.size linkView.sizeToFit()今回は
linkView
のサイズをSuperview
に揃えましたが、任意のCGSize
を設定しても◎
これにて完成です。ソースコード全体
let metadataProvider = LPMetadataProvider() let url = URL(string: "https://youtu.be/hMgcaYd2BZM")! metadataProvider.startFetchingMetadata( for: url, completionHandler: { metadata, error in if error != nil { return } guard let metadata = metadata else { return } DispatchQueue.main.async { let linkView = LPLinkView(metadata: metadata) self.previewView.addSubview(linkView) linkView.frame.size = self.previewView.frame.size linkView.sizeToFit() } })ぴったり14行✌️?
まとめ
iOS14のリリースを(おそらく)9月に控え、最新OSを2,3世代サポートしているアプリは、やっとiOS13以降のフレームワークを本格的に導入していけることと思います。
LinkPresentationは たった十数行でアプリのUXを向上できるフレームワークなので、ぜひ活用していきたいですね。このフレームワークについては、2019年のWWDCビデオもあります。
ビデオの長さは6分前後なので、気軽に見られておすすめです。
今回の記事は触れないローカルキャッシュの取得や共有時の実装のポイントに触れられています。
https://developer.apple.com/videos/play/wwdc2019/262/以上です
ありがとうございました。