Sponsor Link
環境&対象
- macOS Ventura 13.4
- Xcode 14.3.1
以前の記事
以前にも AtCoder 向け環境セットアップの記事を書いてます。
[Swift] AtCoder で Swift を使って UnitTest しながら実装する方法
ですが、自分で読み直してみて わかりにくい部分があったので、改めて記事にしてみます。
AtCoder
サイトは、こちら。
問題が出されて、その問題を解くためのコードを提出すると採点してくれるサイトです。Web 上のテキストフィールドにコードを入れると、実行して コードの採点をしてくれます。その時に、様々な言語を指定することができ、その中の1つに Swift があります。
前回の記事を書いた頃に、個人的に盛り上がっていました。ですが Swift で使用できる 標準ライブラリ的なもの皆無であることもあり、数回参加してやめてしまいました。例えば、C++ は、STL に便利なメソッドが多く用意されているのと比較して、Swift は 自前で用意しなければいけないメソッドが多く 面倒になってしまいました。
ですが、先日なんとなく 確認してみたところ、Swift-Collections や Swift-algorithms を使用したコードが使えるようになるみたいです。(2023.5.31 時点では、いつから利用可能かはよくわかりませんでした)
ということで、再度(?) やる気になったので、環境を再構築しようと思ったのですが、(自分で書いたにも関わらず) 以前の記事は 暗黙の前提が多くわかりにくかったので この記事で あらためて まとめています。
Xcode での環境構築(UnitTest 含む)
AtCoder では、 Browser 上にコピペしたコードが、サーバー上でコンパイル/リンク等されて実行されます。そのため、UI を持つアプリではなく、コマンドラインツールとしてのコードになっている必要があります。
つまり、明示的な main から実行されるコードです。
CommandLineToolプロジェクト
Xcode では、”Command Line Tool” 向け プロジェクトテンプレートを使用してセットアップします。

あとは、適当に名前をつけて、保存場所を指定すればプロジェクトが作成されます。
標準では main.swift が作成されて、以下のような中身になっています。
//
// main.swift
//
// Created by : Tomoaki Yagishita on 2023/05/31
// © 2023 SmallDeskSoftware
//
import Foundation
print("Hello, World!")
Swift コンパイラーは、main.swift があると、そのファイルから実行を開始します。
つまり、このプロジェクトをコンパイルして、作成されたバイナリを実行すると “Hello, World!” と表示されるということです。
@main という annotation で 実行開始位置を指定することもできますが、main.swift ファイルの存在とは排他的なオプションになっているようです。
UnitTestBundle 追加
UnitTest を使って進めていきたいので、最初に、 Unit Testing Bundle を追加してしまいます。
ここまでの操作で、ロジックを UnitTest しつつ、提出用コードを作る準備ができました。
SwiftPackage の追加
swift-collections, swift-algorithms を使用できるようになるようなので、プロジェクトに Swift Package を追加します。
SwiftPackage の追加ターゲットは、Command Line Tool ではなく UnitTest です。
自環境では、UnitTest 時に参照できればよく、Command Line Tool としてコンパイルされることは、自環境上ではありません。
執筆時点(2023.5.31) では、swift-collections, swift-algorithms いずれも AtCoder では使用できないので、将来的な準備でしかありません。
自分の環境としては、上記をセットアップした git リポジトリを用意しておいて、必要に応じて branch を作成して使用しています。
プロジェクトのテンプレートを作りたいと思ったのですが、情報が少なくできませんでした・・・orz
2つのファイルの使い分け
# 自分の備忘録として、実装する時の方向性もメモしておきます。
プロジェクト内には2つのファイルがあります。
– main.swift
– <UnitTestターゲット名>.swift
main.swift には、main 関数と、ロジック実装関数 Func を実装していきます。
<UnitTestターゲット名>.swift には、Func 関数の UnitTest を記述する形で進めます。
どちらに何を書くかという基準は、main.swift をまるごとコピーして、AtCoder のサイトに貼り付けることで提出できるかどうかです。(必要なものは、main.swift に入れる)
AtCoder の課題は、複数行の入力をもとに課題の解を求め、結果を標準出力に出力します。
標準入出力を使った関数は、UnitTest が手間なので、Func(_ lines:[String]) -> [String] なる関数を UnitTest 対象にしてテストを行い、以下のような main 関数と合わせて、AtCoder に提出することで動作させます。
以下は、こちらの問題を解いてみたものです。
//
// main.swift
//
// Created by : Tomoaki Yagishita on 2023/05/31
// © 2023 SmallDeskSoftware
//
import Foundation
func main() {
guard let line0 = readLine() else { fatalError("no input")}
guard let line1 = readLine() else { fatalError("no input")}
guard let line2 = readLine() else { fatalError("no input")}
let lines = [line0, line1, line2]
let results = Func(lines)
for result in results {
print(result)
}
}
#if !DEBUG
main()
#endif
func Func(_ lines: [String]) -> [String] {
let ones = ["1", "l"]
let zeros = ["0", "o"]
for pair in zip(lines[1], lines[2]) {
guard (pair.0 == pair.1) ||
(ones.contains(String(pair.0)) && ones.contains(String(pair.1))) ||
(zeros.contains(String(pair.0)) && zeros.contains(String(pair.1))) else { return ["No"] }
}
return ["Yes"]
}
UnitTest 側のファイルは、以下のような形で使用します。
//
// UnitTest.swift
//
// Created by : Tomoaki Yagishita on 2023/05/31
// © 2023 SmallDeskSoftware
//
import XCTest
final class UnitTest: XCTestCase {
func testExample() throws {
let input1 = """
3
l0w
1ow
""".split(separator: "\n").map{ String($0)}
XCTAssertEqual(Func(input1), ["Yes"])
let input2 = """
3
abc
arc
""".split(separator: "\n").map{ String($0)}
XCTAssertEqual(Func(input2), ["No"])
let input3 = """
4
nok0
n0ko
""".split(separator: "\n").map{ String($0)}
XCTAssertEqual(Func(input3), ["Yes"])
}
}
なんとなく(?) 1つの 関数で例題として与えられているケース全てを実行してしまっていますが、複数のテストに分割しても もちろん 問題ありません。
main.swift 中の main
標準入力から読み込み、[String] にして、ロジック関数に渡し、ロジック関数からの返り値を標準出力に出力することを行います。
AtCoder での入力は、固定行数/可変行数 と大別できます。
readLine という標準入力から読み込み、String? で返す関数を使って、標準入力を [String] に整えます。
func main() {
guard let line0 = readLine() else { fatalError("no input")}
guard let line1 = readLine() else { fatalError("no input")}
guard let line2 = readLine() else { fatalError("no input")}
let lines = [line0, line1, line2]
let results = Func(lines)
for result in results {
print(result)
}
}
上記は、3行の入力があるケースで、3行渡されることを決めうちしてコードを書いています。
# 実際のアプリでは、エラー処理必須だと思いますが、AtCoder では 前提条件を満たさない入力を考慮する必要はありません。
可変行数を読み込むことが必要なケースでは、それ以前に読み込むべき行数が与えられるはずなので、その情報を使って読み込み、[String] を作成します。
guard let line0 = readLine() else { fatalError("no input")}
let lineNum = line0.components(separatedBy: " ").map{Int($0)!}.dropFirst().first!var lines = [line0]
for _ in 0..
問題によっては、1行に複数の情報を与えられることもあります。例えば上記は、1行目の 2つ目の数値がそれ以降に与えられる情報の行数である場合です。(問題によって変わりますので、適切な String 関連の関数を使って、読み取る必要があります。)
main.swift 中の Func
Func は、AtCoder で要求されるロジックを記述する関数です。
# 特に名前に制約はありませんが、毎回考えるのが手間&コピペミス等を防げるため、関数名を固定してしまっています。
func Func(_ lines: [String]) -> [String] {
let ones = ["1", "l"]
let zeros = ["0", "o"]
for pair in zip(lines[1], lines[2]) {
guard (pair.0 == pair.1) ||
(ones.contains(String(pair.0)) && ones.contains(String(pair.1))) ||
(zeros.contains(String(pair.0)) && zeros.contains(String(pair.1))) else { return ["No"] }
}
return ["Yes"]
}
Swift では、String に対して 配列的にアクセスできず、String.Index や Range<String.Index> を使用しなければいけないことが 少し手間に感じますが、諦めるしかありません。
main.swift へのライブラリコピー
自前のライブラリ等を使っている場合は、main.swift へコピーしておきます。
main.swift をまるっと 提出できる状態にしておくと、提出作業が
「main.swift を選択し、⌘-a(全選択) -> ⌘-c(コピー) -> (AtCoder のサイトへ移動) -> ⌘-v(ペースト) 」
という固定作業にできるので、間違いを少なくできます。
UnitTest 中のテスト
UnitTest は、先に例を挙げましたが、以下のようになります。
//
// UnitTest.swift
//
// Created by : Tomoaki Yagishita on 2023/05/31
// © 2023 SmallDeskSoftware
//
import XCTest
final class UnitTest: XCTestCase {
func testExample() throws {
let input1 = """
3
l0w
1ow
""".split(separator: "\n").map{ String($0)}
XCTAssertEqual(Func(input1), ["Yes"])
}
}
上記は、以下の3行が入力として与えられ、
3
l0w
1ow
次の行が出力されることをテストするものです。
Yes
Func の入力を [String]と決め打ちしているので、入力を [String] で作成して、Func を呼び出します。
Func の出力も [String] と決め打ちしているので、その結果が正しいかどうかを [String] の比較でテストします。
Func の出力は、[String] ですが、課題によっては、実数計算が必要になることがあり、出力も Double を String に変換したものが必要になることがあります。そのような場合は、内部の計算誤差により、String も例題の回答と少し異なることがあります。
実際の計算結果を確認して誤差と考えられるのであれば、テストを調整する必要があります。
AtCoder Beginner Contest303-B をやってみる
実際に、使用シーンを想定して使ってみます。
お題は、こちら。
UnitTest
せっかく UnitTest しやすい環境を作っているので、まずは、UnitTest を書きます。
AtCoder の問題は、例題がついているので、例題をテストとして書きます。
その他、自分の思いついたロジックで、コーナーケースを思いつくならそのテストも書いたほうが良いかもしれません。
この問題では、3つの例題とその回答が与えられていますので、そのまま使っています。
final class UnitTest: XCTestCase {
func testExample() throws {
let input1 = """
4 2
1 2 3 4
4 3 1 2
""".split(separator: "\n").map{ String($0)}
XCTAssertEqual(Func(input1), ["2"])
let input2 = """
3 3
1 2 3
3 1 2
1 2 3
""".split(separator: "\n").map{ String($0)}
XCTAssertEqual(Func(input2), ["0"])
let input3 = """
10 10
4 10 7 2 8 3 9 1 6 5
3 6 2 9 1 8 10 7 4 5
9 3 4 5 7 10 1 8 2 6
7 3 1 8 4 9 5 6 2 10
5 2 1 4 10 7 9 8 3 6
5 8 1 6 9 3 2 4 7 10
8 10 3 4 5 7 2 9 6 1
3 10 2 7 8 5 1 4 9 6
10 6 1 5 4 2 3 8 9 7
4 5 9 1 8 2 7 6 3 10
""".split(separator: "\n").map{ String($0)}
XCTAssertEqual(Func(input3), ["6"])
}
}
なお、Swift で複数行の String 定義を行うときは、””” を使うと便利です。
[Swift] String の定義方法
Func からは [String] で返されることに気をつけて XCTAssertEqual を使います。
# この問題では、Int を返しても良いですが、都度変更していると書き換えが手間なので、返り値も [String] で固定しています。
まだ コンパイルすら通りませんが、テストができたので、実装していきます。
main の修正
問題文を読むとわかりますが、可変行が渡されます。
1行目に、各行にいくつの情報が含まれているか と 何行あるか が渡されます。
その情報を使って、以降の行を読むことが必要です。
func main() {
guard let line0 = readLine() else { fatalError("no input")}
let lineNum = line0.components(separatedBy: " ").map{Int($0)!}.dropFirst().first!
var lines = [line0]
for _ in 0..<lineNum {
guard let line = readLine() else { fatalError("no input")}
lines.append(line)
}
let results = Func(lines)
for result in results {
print(result)
}
}
1行目の2つ目の数字が、以降の行数です。その数だけ読んで、Func を呼び出します。
入力には、パターンがあるので、main 関数は、複数のテンプレートを用意しておくと、選ぶだけにすることも可能です。
Func の修正
# この記事執筆時点では、swift-collections や swift-algorithms は使えませんでした。
# 配列中の前後要素を使った処理のために、自前の PairIterator を使用しています。
方向性としては、以下です。
・各行を調べて友達のペアを確認する
・確認したペア情報を Set に入れる
・最後に Set の要素数を確認することで、友達のペア数を確認する
各行を調べて友達のペアを確認する
PairIterator は、[1,2,3,4,5] という配列に対して、(1,2), (2,3), (3,4), (4,5), (5,nil), nil というペアを返してくる Iterator なので、取得できるペアを Set に保存しつつ、最後のペア(5,nil)を破棄することで、友達のペアすべてを確認できることになります。
確認したペア情報を Set に入れる
課題には、「人 x と人 y からなる二人組と人 y と人 x からなる二人組は区別しません」 という条件があります。二人組をあらわす FriendPair を生成する時に、一人目を二人目より小さな数字になるように調整することで、同一視できるようにし、さらに、Set に保存することで重複が無いようにします。
こうすることで、(x,y) と (y,x) を同一視できるようになります。
なお、Set の要素は、Hashable でなければいけません。
各行を確認した後、最終的に Set に保存されている要素数が 友達と考えられるペアの数です。
N 人の人がいる時に 考えられる最大の友達ペアの数は N * (N-1) / 2 ですから、不仲(?)と考えられるペアの数は N * (N-1) / 2 から Set の要素数を引くと得られます。
ということで、Func は、以下のようになります。最終的に [String] として返すようにしています。
struct FriendPair: Hashable {
let one: Int
let two: Int
init(_ one: Int,_ two: Int) { // always one two
self.one = min(one, two)
self.two = max(one, two)
}
}
func Func(_ lines: [String]) -> [String] {
let numOfPerson = Int(lines[0].components(separatedBy: " ").first!)!
var friendSet: Set = Set()
for line in lines.dropFirst() {
let pairs = line.components(separatedBy: " ").map({Int($0)!})
var pairIte = pairs.makePairIterator()
while let (current, next) = pairIte.next() {
guard let next = next else { break }
friendSet.insert(FriendPair(current, next))
}
}
return [String(numOfPerson * (numOfPerson - 1) / 2 - friendSet.count)]
}
// copy from another library
public struct PairIterator{
let collection: C
var currentIterator: C.Iterator
var nextIterator: C.Iterator
public init(_ collection: C) {
self.collection = collection
currentIterator = collection.makeIterator()
nextIterator = collection.makeIterator()
_ = nextIterator.next()
}
public mutating func next() -> (C.Element, C.Element?)? {
guard let current = currentIterator.next() else { return nil }
let next = nextIterator.next()
return (current, next)
}
}
extension Array {
public func makePairIterator() -> PairIterator {
return PairIterator(self)
}
}
あとは、Target に UnitTest を選択してから、コンパイル -> テスト -> 修正 を繰り返していくだけです。
Target に UnitTest を選択して、⌘-U を押下すると (コンパイル対象に設定しているので) main.swift もコンパイルされますので、ターゲットに Command Line Tool を選択することは不要となります。
まとめ
AtCoder 向けのテスト環境構築を改めて書きました。
- Command Line Tool プロジェクトを使用する
- Unit Testing Bundle は、後から追加する必要がある
その時、Target to be tested は、None 指定 - テストしたい関数を含む main.swift を UnitTest ターゲットにも追加する
- 標準入力読み込みは、readLine が便利
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
SwiftUI おすすめ本
SwiftUI を理解するには、以下の本がおすすめです。
SwiftUI ViewMatery
SwiftUI で開発していくときに、ViewやLayoutのための適切なmodifierを探すのが大変です。
英語での説明になってしまいますが、以下の”SwiftUI Views Mastery Bundle”という本がビジュアル的に確認して探せるので、便利です。
英語ではありますが、1ページに コードと画面が並んでいるので、非常にわかりやすいです。
View に適用できる modifier もわかりやすく説明されているので、ビューの理解だけではなく、どのような装飾ができるかも簡単にわかります。
超便利です
販売元のページは、こちらです。
SwiftUI 徹底入門
# SwiftUI は、毎年大きく改善されていますので、少し古くなってしまいましたが、いまでも 定番本です。
Swift学習におすすめの本
詳解Swift
Swift の学習には、詳解 Swift という書籍が、おすすめです。
著者は、Swift の初期から書籍を出していますし、Swift の前に主力言語だった Objective-C という言語についても同様の書籍を出しています。
最新版を購入するのがおすすめです。
現時点では、上記の Swift 5 に対応した第5版が最新版です。
Sponsor Link