├── .gitignore ├── .swift-version ├── .swiftlint.yml ├── .travis.yml ├── Cartfile.private ├── Cartfile.resolved ├── DEMO ├── .swiftlint.yml ├── KRTournamentViewDemo.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── KRTournamentViewDemo.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── KRTournamentViewDemo │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ └── edit.imageset │ │ │ ├── Contents.json │ │ │ └── edit.pdf │ ├── BracketVC.swift │ ├── Info.plist │ ├── KRTournamentViewStyle+Ex.swift │ ├── Launch Screen.storyboard │ ├── Main.storyboard │ ├── MainVC.swift │ ├── MyMatch.swift │ └── StageView.swift ├── Podfile └── Podfile.lock ├── Documentation ├── En │ ├── Builder.md │ ├── HowToUse.md │ ├── README.md │ └── Style.md └── Ja │ ├── Builder.md │ ├── HowToUse.md │ ├── README.md │ └── Style.md ├── KRTournamentView.podspec ├── KRTournamentView.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── KRTournamentView.xcscheme ├── KRTournamentView ├── Classes │ ├── KRTournamentView.swift │ ├── KRTournamentViewDataSource.swift │ ├── KRTournamentViewDelegate.swift │ ├── KRTournamentViewEntry.swift │ ├── KRTournamentViewEntryLabel.swift │ ├── KRTournamentViewMatch.swift │ ├── KRTournamentViewStyle.swift │ ├── MatchPath.swift │ ├── Private │ │ ├── Bracket+PathSet.swift │ │ ├── Defaults.swift │ │ ├── KRTournamentDrawingView.swift │ │ ├── KRTournamentEntriesView.swift │ │ ├── KRTournamentViewDataStore.swift │ │ ├── NSLayoutConstraint+Ex.swift │ │ ├── PathSet.swift │ │ └── TournamentInfo.swift │ └── TournamentStructure │ │ ├── Bracket.swift │ │ ├── Entry.swift │ │ ├── TournamentBuilder.swift │ │ └── TournamentStructure.swift ├── Info.plist └── KRTournamentView.h ├── KRTournamentViewTests ├── Fakes │ └── KRTournamentViewEntryFake.swift ├── Info.plist ├── Mocks │ ├── KRTournamentViewDataSourceMock.swift │ └── KRTournamentViewDataStoreMock.swift └── Tests │ ├── Bracket+PathSetTests.swift │ ├── BracketTests.swift │ ├── EntryTests.swift │ ├── KRTournamentEntriesViewTests.swift │ ├── KRTournamentViewEntryLabelTests.swift │ ├── KRTournamentViewEntryTests.swift │ ├── KRTournamentViewStyleTests.swift │ ├── KRTournamentViewTests.swift │ ├── MatchPathTests.swift │ ├── PathSetTests.swift │ ├── TournamentBuilderTests.swift │ ├── TournamentInfoTests.swift │ └── TournamentStructureTests.swift ├── LICENSE ├── Package.swift ├── README.md └── README_Ja.md /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata 15 | *.xccheckout 16 | profile 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | 22 | # Bundler 23 | .bundle 24 | 25 | Carthage/ 26 | Pods/ 27 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.0.1 2 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - line_length 3 | - file_length 4 | included: 5 | - KRTournamentView 6 | - KRTournamentViewTests 7 | excluded: 8 | - Pods 9 | type_body_length: 10 | - 400 # warning 11 | - 500 # error 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode11.3 3 | xcode_sdk: iphonesimulator13.2 4 | install: 5 | - gem install xcpretty 6 | - carthage bootstrap --platform iOS 7 | script: 8 | - set -o pipefail 9 | - travis_retry xcodebuild -project KRTournamentView.xcodeproj -scheme KRTournamentView -destination "platform=iOS Simulator,name=iPhone 11" build-for-testing test | xcpretty 10 | notifications: 11 | email: false 12 | -------------------------------------------------------------------------------- /Cartfile.private: -------------------------------------------------------------------------------- 1 | github "Quick/Quick" 2 | github "Quick/Nimble" 3 | -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "Quick/Nimble" "v8.0.4" 2 | github "Quick/Quick" "v2.2.0" 3 | -------------------------------------------------------------------------------- /DEMO/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - line_length 3 | - file_length 4 | type_body_length: 5 | - 400 # warning 6 | - 500 # error 7 | -------------------------------------------------------------------------------- /DEMO/KRTournamentViewDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 6B50CD28201A3DA7005C4179 /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6B50CD27201A3DA7005C4179 /* Launch Screen.storyboard */; }; 11 | 6BC0BC302019ED750033FEC1 /* StageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BC0BC2F2019ED750033FEC1 /* StageView.swift */; }; 12 | 6BC0BC352019F7140033FEC1 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6BC0BC332019F7140033FEC1 /* Main.storyboard */; }; 13 | 6BDB17AB2288181A00A1E4E5 /* KRTournamentViewStyle+Ex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BDB17AA2288181A00A1E4E5 /* KRTournamentViewStyle+Ex.swift */; }; 14 | 6BDB17AD22881A6200A1E4E5 /* MyMatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BDB17AC22881A6200A1E4E5 /* MyMatch.swift */; }; 15 | 6BDB17AF2288456000A1E4E5 /* BracketVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BDB17AE2288456000A1E4E5 /* BracketVC.swift */; }; 16 | 6BF4AED01FD579E1005EDCE0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BF4AECF1FD579E1005EDCE0 /* AppDelegate.swift */; }; 17 | 6BF4AED21FD579E1005EDCE0 /* MainVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BF4AED11FD579E1005EDCE0 /* MainVC.swift */; }; 18 | 6BF4AED71FD579E1005EDCE0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6BF4AED61FD579E1005EDCE0 /* Assets.xcassets */; }; 19 | 916C8D9DB1D4C2B86BF04097 /* Pods_KRTournamentViewDemo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2EC14EBB87C1C813A0E0ED6 /* Pods_KRTournamentViewDemo.framework */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXFileReference section */ 23 | 1E50F977C05A73112F3733B8 /* Pods-KRTournamentViewDemo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-KRTournamentViewDemo.debug.xcconfig"; path = "Target Support Files/Pods-KRTournamentViewDemo/Pods-KRTournamentViewDemo.debug.xcconfig"; sourceTree = ""; }; 24 | 6B50CD27201A3DA7005C4179 /* Launch Screen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = ""; }; 25 | 6BC0BC2F2019ED750033FEC1 /* StageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StageView.swift; sourceTree = ""; }; 26 | 6BC0BC332019F7140033FEC1 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; 27 | 6BDB17AA2288181A00A1E4E5 /* KRTournamentViewStyle+Ex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "KRTournamentViewStyle+Ex.swift"; sourceTree = ""; }; 28 | 6BDB17AC22881A6200A1E4E5 /* MyMatch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyMatch.swift; sourceTree = ""; }; 29 | 6BDB17AE2288456000A1E4E5 /* BracketVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BracketVC.swift; sourceTree = ""; }; 30 | 6BF4AECC1FD579E1005EDCE0 /* KRTournamentViewDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KRTournamentViewDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 31 | 6BF4AECF1FD579E1005EDCE0 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 32 | 6BF4AED11FD579E1005EDCE0 /* MainVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainVC.swift; sourceTree = ""; }; 33 | 6BF4AED61FD579E1005EDCE0 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 34 | 6BF4AEDB1FD579E1005EDCE0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 35 | A85706E02E63AD483C5181AF /* Pods-KRTournamentViewDemo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-KRTournamentViewDemo.release.xcconfig"; path = "Target Support Files/Pods-KRTournamentViewDemo/Pods-KRTournamentViewDemo.release.xcconfig"; sourceTree = ""; }; 36 | D2EC14EBB87C1C813A0E0ED6 /* Pods_KRTournamentViewDemo.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_KRTournamentViewDemo.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 37 | /* End PBXFileReference section */ 38 | 39 | /* Begin PBXFrameworksBuildPhase section */ 40 | 6BF4AEC91FD579E1005EDCE0 /* Frameworks */ = { 41 | isa = PBXFrameworksBuildPhase; 42 | buildActionMask = 2147483647; 43 | files = ( 44 | 916C8D9DB1D4C2B86BF04097 /* Pods_KRTournamentViewDemo.framework in Frameworks */, 45 | ); 46 | runOnlyForDeploymentPostprocessing = 0; 47 | }; 48 | /* End PBXFrameworksBuildPhase section */ 49 | 50 | /* Begin PBXGroup section */ 51 | 14C86BA82CF981367FD97366 /* Pods */ = { 52 | isa = PBXGroup; 53 | children = ( 54 | 1E50F977C05A73112F3733B8 /* Pods-KRTournamentViewDemo.debug.xcconfig */, 55 | A85706E02E63AD483C5181AF /* Pods-KRTournamentViewDemo.release.xcconfig */, 56 | ); 57 | path = Pods; 58 | sourceTree = ""; 59 | }; 60 | 6BADBA9A22535D1900CBEE76 /* Frameworks */ = { 61 | isa = PBXGroup; 62 | children = ( 63 | D2EC14EBB87C1C813A0E0ED6 /* Pods_KRTournamentViewDemo.framework */, 64 | ); 65 | name = Frameworks; 66 | sourceTree = ""; 67 | }; 68 | 6BF4AEC31FD579E1005EDCE0 = { 69 | isa = PBXGroup; 70 | children = ( 71 | 6BF4AECE1FD579E1005EDCE0 /* KRTournamentViewDemo */, 72 | 6BF4AECD1FD579E1005EDCE0 /* Products */, 73 | 6BADBA9A22535D1900CBEE76 /* Frameworks */, 74 | 14C86BA82CF981367FD97366 /* Pods */, 75 | ); 76 | sourceTree = ""; 77 | }; 78 | 6BF4AECD1FD579E1005EDCE0 /* Products */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | 6BF4AECC1FD579E1005EDCE0 /* KRTournamentViewDemo.app */, 82 | ); 83 | name = Products; 84 | sourceTree = ""; 85 | }; 86 | 6BF4AECE1FD579E1005EDCE0 /* KRTournamentViewDemo */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | 6BF4AECF1FD579E1005EDCE0 /* AppDelegate.swift */, 90 | 6BDB17AA2288181A00A1E4E5 /* KRTournamentViewStyle+Ex.swift */, 91 | 6BDB17AC22881A6200A1E4E5 /* MyMatch.swift */, 92 | 6BC0BC2F2019ED750033FEC1 /* StageView.swift */, 93 | 6BF4AED11FD579E1005EDCE0 /* MainVC.swift */, 94 | 6BDB17AE2288456000A1E4E5 /* BracketVC.swift */, 95 | 6BC0BC332019F7140033FEC1 /* Main.storyboard */, 96 | 6B50CD27201A3DA7005C4179 /* Launch Screen.storyboard */, 97 | 6BF4AED61FD579E1005EDCE0 /* Assets.xcassets */, 98 | 6BF4AEDB1FD579E1005EDCE0 /* Info.plist */, 99 | ); 100 | path = KRTournamentViewDemo; 101 | sourceTree = ""; 102 | }; 103 | /* End PBXGroup section */ 104 | 105 | /* Begin PBXNativeTarget section */ 106 | 6BF4AECB1FD579E1005EDCE0 /* KRTournamentViewDemo */ = { 107 | isa = PBXNativeTarget; 108 | buildConfigurationList = 6BF4AEDE1FD579E1005EDCE0 /* Build configuration list for PBXNativeTarget "KRTournamentViewDemo" */; 109 | buildPhases = ( 110 | 73E028FB053A7CE71D13095F /* [CP] Check Pods Manifest.lock */, 111 | 6BF4AEC81FD579E1005EDCE0 /* Sources */, 112 | 6BF4AEC91FD579E1005EDCE0 /* Frameworks */, 113 | 6BF4AECA1FD579E1005EDCE0 /* Resources */, 114 | 6BCC9CA21FD57BB400EE7DEC /* ShellScript */, 115 | 44B114F3BD30D29F1AD8ABFB /* [CP] Embed Pods Frameworks */, 116 | ); 117 | buildRules = ( 118 | ); 119 | dependencies = ( 120 | ); 121 | name = KRTournamentViewDemo; 122 | productName = KRTournamentViewDemo; 123 | productReference = 6BF4AECC1FD579E1005EDCE0 /* KRTournamentViewDemo.app */; 124 | productType = "com.apple.product-type.application"; 125 | }; 126 | /* End PBXNativeTarget section */ 127 | 128 | /* Begin PBXProject section */ 129 | 6BF4AEC41FD579E1005EDCE0 /* Project object */ = { 130 | isa = PBXProject; 131 | attributes = { 132 | LastSwiftUpdateCheck = 0910; 133 | LastUpgradeCheck = 0930; 134 | ORGANIZATIONNAME = Krimpedance; 135 | TargetAttributes = { 136 | 6BF4AECB1FD579E1005EDCE0 = { 137 | CreatedOnToolsVersion = 9.1; 138 | ProvisioningStyle = Automatic; 139 | }; 140 | }; 141 | }; 142 | buildConfigurationList = 6BF4AEC71FD579E1005EDCE0 /* Build configuration list for PBXProject "KRTournamentViewDemo" */; 143 | compatibilityVersion = "Xcode 11.0"; 144 | developmentRegion = en; 145 | hasScannedForEncodings = 0; 146 | knownRegions = ( 147 | en, 148 | Base, 149 | ); 150 | mainGroup = 6BF4AEC31FD579E1005EDCE0; 151 | productRefGroup = 6BF4AECD1FD579E1005EDCE0 /* Products */; 152 | projectDirPath = ""; 153 | projectRoot = ""; 154 | targets = ( 155 | 6BF4AECB1FD579E1005EDCE0 /* KRTournamentViewDemo */, 156 | ); 157 | }; 158 | /* End PBXProject section */ 159 | 160 | /* Begin PBXResourcesBuildPhase section */ 161 | 6BF4AECA1FD579E1005EDCE0 /* Resources */ = { 162 | isa = PBXResourcesBuildPhase; 163 | buildActionMask = 2147483647; 164 | files = ( 165 | 6B50CD28201A3DA7005C4179 /* Launch Screen.storyboard in Resources */, 166 | 6BC0BC352019F7140033FEC1 /* Main.storyboard in Resources */, 167 | 6BF4AED71FD579E1005EDCE0 /* Assets.xcassets in Resources */, 168 | ); 169 | runOnlyForDeploymentPostprocessing = 0; 170 | }; 171 | /* End PBXResourcesBuildPhase section */ 172 | 173 | /* Begin PBXShellScriptBuildPhase section */ 174 | 44B114F3BD30D29F1AD8ABFB /* [CP] Embed Pods Frameworks */ = { 175 | isa = PBXShellScriptBuildPhase; 176 | buildActionMask = 2147483647; 177 | files = ( 178 | ); 179 | inputFileListPaths = ( 180 | "${PODS_ROOT}/Target Support Files/Pods-KRTournamentViewDemo/Pods-KRTournamentViewDemo-frameworks-${CONFIGURATION}-input-files.xcfilelist", 181 | ); 182 | name = "[CP] Embed Pods Frameworks"; 183 | outputFileListPaths = ( 184 | "${PODS_ROOT}/Target Support Files/Pods-KRTournamentViewDemo/Pods-KRTournamentViewDemo-frameworks-${CONFIGURATION}-output-files.xcfilelist", 185 | ); 186 | runOnlyForDeploymentPostprocessing = 0; 187 | shellPath = /bin/sh; 188 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-KRTournamentViewDemo/Pods-KRTournamentViewDemo-frameworks.sh\"\n"; 189 | showEnvVarsInLog = 0; 190 | }; 191 | 6BCC9CA21FD57BB400EE7DEC /* ShellScript */ = { 192 | isa = PBXShellScriptBuildPhase; 193 | buildActionMask = 2147483647; 194 | files = ( 195 | ); 196 | inputPaths = ( 197 | ); 198 | outputPaths = ( 199 | ); 200 | runOnlyForDeploymentPostprocessing = 0; 201 | shellPath = /bin/sh; 202 | shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"SwiftLint does not exist, download from https://github.com/realm/SwiftLint\"\nfi\n"; 203 | }; 204 | 73E028FB053A7CE71D13095F /* [CP] Check Pods Manifest.lock */ = { 205 | isa = PBXShellScriptBuildPhase; 206 | buildActionMask = 2147483647; 207 | files = ( 208 | ); 209 | inputFileListPaths = ( 210 | ); 211 | inputPaths = ( 212 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 213 | "${PODS_ROOT}/Manifest.lock", 214 | ); 215 | name = "[CP] Check Pods Manifest.lock"; 216 | outputFileListPaths = ( 217 | ); 218 | outputPaths = ( 219 | "$(DERIVED_FILE_DIR)/Pods-KRTournamentViewDemo-checkManifestLockResult.txt", 220 | ); 221 | runOnlyForDeploymentPostprocessing = 0; 222 | shellPath = /bin/sh; 223 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 224 | showEnvVarsInLog = 0; 225 | }; 226 | /* End PBXShellScriptBuildPhase section */ 227 | 228 | /* Begin PBXSourcesBuildPhase section */ 229 | 6BF4AEC81FD579E1005EDCE0 /* Sources */ = { 230 | isa = PBXSourcesBuildPhase; 231 | buildActionMask = 2147483647; 232 | files = ( 233 | 6BF4AED21FD579E1005EDCE0 /* MainVC.swift in Sources */, 234 | 6BDB17AB2288181A00A1E4E5 /* KRTournamentViewStyle+Ex.swift in Sources */, 235 | 6BDB17AD22881A6200A1E4E5 /* MyMatch.swift in Sources */, 236 | 6BDB17AF2288456000A1E4E5 /* BracketVC.swift in Sources */, 237 | 6BC0BC302019ED750033FEC1 /* StageView.swift in Sources */, 238 | 6BF4AED01FD579E1005EDCE0 /* AppDelegate.swift in Sources */, 239 | ); 240 | runOnlyForDeploymentPostprocessing = 0; 241 | }; 242 | /* End PBXSourcesBuildPhase section */ 243 | 244 | /* Begin XCBuildConfiguration section */ 245 | 6BF4AEDC1FD579E1005EDCE0 /* Debug */ = { 246 | isa = XCBuildConfiguration; 247 | buildSettings = { 248 | ALWAYS_SEARCH_USER_PATHS = NO; 249 | CLANG_ANALYZER_NONNULL = YES; 250 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 251 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 252 | CLANG_CXX_LIBRARY = "libc++"; 253 | CLANG_ENABLE_MODULES = YES; 254 | CLANG_ENABLE_OBJC_ARC = YES; 255 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 256 | CLANG_WARN_BOOL_CONVERSION = YES; 257 | CLANG_WARN_COMMA = YES; 258 | CLANG_WARN_CONSTANT_CONVERSION = YES; 259 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 260 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 261 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 262 | CLANG_WARN_EMPTY_BODY = YES; 263 | CLANG_WARN_ENUM_CONVERSION = YES; 264 | CLANG_WARN_INFINITE_RECURSION = YES; 265 | CLANG_WARN_INT_CONVERSION = YES; 266 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 267 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 268 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 269 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 270 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 271 | CLANG_WARN_STRICT_PROTOTYPES = YES; 272 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 273 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 274 | CLANG_WARN_UNREACHABLE_CODE = YES; 275 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 276 | CODE_SIGN_IDENTITY = "iPhone Developer"; 277 | COPY_PHASE_STRIP = NO; 278 | DEBUG_INFORMATION_FORMAT = dwarf; 279 | ENABLE_STRICT_OBJC_MSGSEND = YES; 280 | ENABLE_TESTABILITY = YES; 281 | GCC_C_LANGUAGE_STANDARD = gnu11; 282 | GCC_DYNAMIC_NO_PIC = NO; 283 | GCC_NO_COMMON_BLOCKS = YES; 284 | GCC_OPTIMIZATION_LEVEL = 0; 285 | GCC_PREPROCESSOR_DEFINITIONS = ( 286 | "DEBUG=1", 287 | "$(inherited)", 288 | ); 289 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 290 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 291 | GCC_WARN_UNDECLARED_SELECTOR = YES; 292 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 293 | GCC_WARN_UNUSED_FUNCTION = YES; 294 | GCC_WARN_UNUSED_VARIABLE = YES; 295 | IPHONEOS_DEPLOYMENT_TARGET = 11.1; 296 | MTL_ENABLE_DEBUG_INFO = YES; 297 | ONLY_ACTIVE_ARCH = YES; 298 | SDKROOT = iphoneos; 299 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 300 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 301 | SWIFT_VERSION = 5.0; 302 | }; 303 | name = Debug; 304 | }; 305 | 6BF4AEDD1FD579E1005EDCE0 /* Release */ = { 306 | isa = XCBuildConfiguration; 307 | buildSettings = { 308 | ALWAYS_SEARCH_USER_PATHS = NO; 309 | CLANG_ANALYZER_NONNULL = YES; 310 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 311 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 312 | CLANG_CXX_LIBRARY = "libc++"; 313 | CLANG_ENABLE_MODULES = YES; 314 | CLANG_ENABLE_OBJC_ARC = YES; 315 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 316 | CLANG_WARN_BOOL_CONVERSION = YES; 317 | CLANG_WARN_COMMA = YES; 318 | CLANG_WARN_CONSTANT_CONVERSION = YES; 319 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 320 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 321 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 322 | CLANG_WARN_EMPTY_BODY = YES; 323 | CLANG_WARN_ENUM_CONVERSION = YES; 324 | CLANG_WARN_INFINITE_RECURSION = YES; 325 | CLANG_WARN_INT_CONVERSION = YES; 326 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 327 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 328 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 329 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 330 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 331 | CLANG_WARN_STRICT_PROTOTYPES = YES; 332 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 333 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 334 | CLANG_WARN_UNREACHABLE_CODE = YES; 335 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 336 | CODE_SIGN_IDENTITY = "iPhone Developer"; 337 | COPY_PHASE_STRIP = NO; 338 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 339 | ENABLE_NS_ASSERTIONS = NO; 340 | ENABLE_STRICT_OBJC_MSGSEND = YES; 341 | GCC_C_LANGUAGE_STANDARD = gnu11; 342 | GCC_NO_COMMON_BLOCKS = YES; 343 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 344 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 345 | GCC_WARN_UNDECLARED_SELECTOR = YES; 346 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 347 | GCC_WARN_UNUSED_FUNCTION = YES; 348 | GCC_WARN_UNUSED_VARIABLE = YES; 349 | IPHONEOS_DEPLOYMENT_TARGET = 11.1; 350 | MTL_ENABLE_DEBUG_INFO = NO; 351 | SDKROOT = iphoneos; 352 | SWIFT_COMPILATION_MODE = wholemodule; 353 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 354 | SWIFT_VERSION = 5.0; 355 | VALIDATE_PRODUCT = YES; 356 | }; 357 | name = Release; 358 | }; 359 | 6BF4AEDF1FD579E1005EDCE0 /* Debug */ = { 360 | isa = XCBuildConfiguration; 361 | baseConfigurationReference = 1E50F977C05A73112F3733B8 /* Pods-KRTournamentViewDemo.debug.xcconfig */; 362 | buildSettings = { 363 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 364 | CODE_SIGN_STYLE = Automatic; 365 | CURRENT_PROJECT_VERSION = 2.0.0; 366 | DEVELOPMENT_TEAM = C9S69ME8A5; 367 | INFOPLIST_FILE = KRTournamentViewDemo/Info.plist; 368 | LD_RUNPATH_SEARCH_PATHS = ( 369 | "$(inherited)", 370 | "@executable_path/Frameworks", 371 | ); 372 | MARKETING_VERSION = 2.0.0; 373 | PRODUCT_BUNDLE_IDENTIFIER = com.krimpedance.KRTournamentViewDemo; 374 | PRODUCT_NAME = "$(TARGET_NAME)"; 375 | SWIFT_VERSION = 5.0; 376 | TARGETED_DEVICE_FAMILY = "1,2"; 377 | }; 378 | name = Debug; 379 | }; 380 | 6BF4AEE01FD579E1005EDCE0 /* Release */ = { 381 | isa = XCBuildConfiguration; 382 | baseConfigurationReference = A85706E02E63AD483C5181AF /* Pods-KRTournamentViewDemo.release.xcconfig */; 383 | buildSettings = { 384 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 385 | CODE_SIGN_STYLE = Automatic; 386 | CURRENT_PROJECT_VERSION = 2.0.0; 387 | DEVELOPMENT_TEAM = C9S69ME8A5; 388 | INFOPLIST_FILE = KRTournamentViewDemo/Info.plist; 389 | LD_RUNPATH_SEARCH_PATHS = ( 390 | "$(inherited)", 391 | "@executable_path/Frameworks", 392 | ); 393 | MARKETING_VERSION = 2.0.0; 394 | PRODUCT_BUNDLE_IDENTIFIER = com.krimpedance.KRTournamentViewDemo; 395 | PRODUCT_NAME = "$(TARGET_NAME)"; 396 | SWIFT_VERSION = 5.0; 397 | TARGETED_DEVICE_FAMILY = "1,2"; 398 | }; 399 | name = Release; 400 | }; 401 | /* End XCBuildConfiguration section */ 402 | 403 | /* Begin XCConfigurationList section */ 404 | 6BF4AEC71FD579E1005EDCE0 /* Build configuration list for PBXProject "KRTournamentViewDemo" */ = { 405 | isa = XCConfigurationList; 406 | buildConfigurations = ( 407 | 6BF4AEDC1FD579E1005EDCE0 /* Debug */, 408 | 6BF4AEDD1FD579E1005EDCE0 /* Release */, 409 | ); 410 | defaultConfigurationIsVisible = 0; 411 | defaultConfigurationName = Release; 412 | }; 413 | 6BF4AEDE1FD579E1005EDCE0 /* Build configuration list for PBXNativeTarget "KRTournamentViewDemo" */ = { 414 | isa = XCConfigurationList; 415 | buildConfigurations = ( 416 | 6BF4AEDF1FD579E1005EDCE0 /* Debug */, 417 | 6BF4AEE01FD579E1005EDCE0 /* Release */, 418 | ); 419 | defaultConfigurationIsVisible = 0; 420 | defaultConfigurationName = Release; 421 | }; 422 | /* End XCConfigurationList section */ 423 | }; 424 | rootObject = 6BF4AEC41FD579E1005EDCE0 /* Project object */; 425 | } 426 | -------------------------------------------------------------------------------- /DEMO/KRTournamentViewDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /DEMO/KRTournamentViewDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /DEMO/KRTournamentViewDemo.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /DEMO/KRTournamentViewDemo.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /DEMO/KRTournamentViewDemo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // KRTournamentViewDemo 4 | // 5 | // Copyright © 2018 Krimpedance. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | @UIApplicationMain 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | return true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /DEMO/KRTournamentViewDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /DEMO/KRTournamentViewDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /DEMO/KRTournamentViewDemo/Assets.xcassets/edit.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "edit.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /DEMO/KRTournamentViewDemo/Assets.xcassets/edit.imageset/edit.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krimpedance/KRTournamentView/fa5f32356b1d84eeb1b612b7f07699aa0905741e/DEMO/KRTournamentViewDemo/Assets.xcassets/edit.imageset/edit.pdf -------------------------------------------------------------------------------- /DEMO/KRTournamentViewDemo/BracketVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BracketVC.swift 3 | // KRTournamentViewDemo 4 | // 5 | // Copyright © 2019 Krimpedance. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | import KRTournamentView 10 | 11 | protocol BracketVCDelegate: class { 12 | func bracketVCDidClose(_ bracketVC: BracketVC) 13 | } 14 | 15 | final class BracketVC: UIViewController { 16 | 17 | @IBOutlet private weak var titleLabel: UILabel! 18 | @IBOutlet private weak var tournamentView: KRTournamentView! 19 | @IBOutlet private weak var entriesLabel: UILabel! 20 | @IBOutlet private weak var winnersLabel: UILabel! 21 | 22 | private var numberOfEntries: Int = 2 { 23 | didSet { 24 | entriesLabel?.text = "\(numberOfEntries)" 25 | tournamentView?.reloadData() 26 | } 27 | } 28 | 29 | private var numberOfWinners: Int = 1 { 30 | didSet { 31 | winnersLabel?.text = "\(numberOfWinners)" 32 | tournamentView?.reloadData() 33 | } 34 | } 35 | 36 | var matchPath: MatchPath! 37 | var builder: TournamentBuilder! 38 | 39 | weak var delegate: BracketVCDelegate? 40 | 41 | override func viewDidLoad() { 42 | super.viewDidLoad() 43 | view.alpha = 0 44 | numberOfEntries = builder.children.count 45 | numberOfWinners = builder.numberOfWinners 46 | tournamentView.dataSource = self 47 | } 48 | 49 | override func viewDidAppear(_ animated: Bool) { 50 | super.viewDidAppear(animated) 51 | 52 | UIView.animate(withDuration: 0.5) { [weak self] in 53 | self?.view.alpha = 1 54 | } 55 | } 56 | } 57 | 58 | // MARK: - Actions ------------ 59 | 60 | extension BracketVC { 61 | @IBAction func entriesButtonTapped(_ button: UIButton) { 62 | let actionSheet = UIAlertController(title: "Number of entries", message: nil, preferredStyle: .actionSheet) 63 | (1..<6).forEach { index in 64 | actionSheet.addAction(UIAlertAction(title: "\(index)", style: .default) { [unowned self] _ in 65 | self.numberOfEntries = index 66 | if self.numberOfWinners > index - 1 { 67 | self.numberOfWinners = max(1, min(index, index - 1)) 68 | } 69 | }) 70 | } 71 | present(actionSheet, animated: true, completion: nil) 72 | } 73 | 74 | @IBAction func winnersButtonTapped(_ button: UIButton) { 75 | let actionSheet = UIAlertController(title: "Number of winenrs", message: nil, preferredStyle: .actionSheet) 76 | (1..<5).forEach { index in 77 | actionSheet.addAction(UIAlertAction(title: "\(index)", style: .default) { [unowned self] _ in 78 | self.numberOfWinners = max(1, min(index, self.numberOfEntries - 1)) 79 | }) 80 | } 81 | present(actionSheet, animated: true, completion: nil) 82 | } 83 | 84 | @IBAction func closeButtonTapped(_ button: UIButton) { 85 | builder.numberOfWinners = numberOfWinners 86 | builder.children = (0.. Bracket { 103 | return TournamentBuilder.build(numberOfLayers: 1, numberOfEntries: numberOfEntries, numberOfWinners: numberOfWinners, handler: nil) 104 | } 105 | 106 | func tournamentView(_ tournamentView: KRTournamentView, entryAt index: Int) -> KRTournamentViewEntry { 107 | return KRTournamentViewEntry() 108 | } 109 | 110 | func tournamentView(_ tournamentView: KRTournamentView, matchAt matchPath: MatchPath) -> KRTournamentViewMatch { 111 | return KRTournamentViewMatch() 112 | } 113 | } 114 | 115 | // MARK: - KRTournament delegate ------------------- 116 | 117 | extension BracketVC: KRTournamentViewDelegate { 118 | func entrySize(in tournamentView: KRTournamentView) -> CGSize { 119 | return .init(width: 0, height: 50) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /DEMO/KRTournamentViewDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | KRTournamentView 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UIRequiresFullScreen 34 | 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /DEMO/KRTournamentViewDemo/KRTournamentViewStyle+Ex.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KRTournamentViewStyle+Ex.swift 3 | // KRTournamentViewDemo 4 | // 5 | // Copyright © 2019 Krimpedance. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | import KRTournamentView 10 | 11 | extension KRTournamentViewStyle { 12 | static let allItems: [KRTournamentViewStyle] = [.left, .right, .top, .bottom, .leftRight, .topBottom] 13 | 14 | var str: String { 15 | switch self { 16 | case .left: return "Left" 17 | case .right: return "Right" 18 | case .top: return "Top" 19 | case .bottom: return "Bottom" 20 | case .leftRight: return "LeftRight" 21 | case .topBottom: return "TopBottom" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /DEMO/KRTournamentViewDemo/Launch Screen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 28 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /DEMO/KRTournamentViewDemo/MainVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainVC.swift 3 | // KRTournamentViewDemo 4 | // 5 | // Copyright © 2018 Krimpedance. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | import KRTournamentView 10 | 11 | final class MainVC: UIViewController { 12 | 13 | @IBOutlet private weak var stageView: StageView! 14 | @IBOutlet private weak var tournamentView: KRTournamentView! 15 | @IBOutlet private weak var styleLabel: UILabel! 16 | @IBOutlet private weak var layerLabel: UILabel! 17 | 18 | private var style: KRTournamentViewStyle = .left { 19 | didSet { 20 | styleLabel.text = style.str 21 | tournamentView.style = style 22 | tournamentView.reloadData() 23 | } 24 | } 25 | 26 | private var numberOfLayers: Int = 3 { 27 | didSet { 28 | layerLabel.text = "\(numberOfLayers)" 29 | builder = .init(numberOfLayers: numberOfLayers) 30 | } 31 | } 32 | 33 | private var builder: TournamentBuilder = .init(numberOfLayers: 3) { 34 | didSet { tournamentView.reloadData() } 35 | } 36 | 37 | private var entryNames = [Int: String]() { 38 | didSet { tournamentView.reloadData() } 39 | } 40 | 41 | override func viewDidLoad() { 42 | super.viewDidLoad() 43 | 44 | tournamentView.dataSource = self 45 | tournamentView.delegate = self 46 | tournamentView.style = style 47 | tournamentView.lineWidth = 2 48 | } 49 | } 50 | 51 | // MARK: - Button actions ------------------- 52 | 53 | extension MainVC { 54 | @IBAction func styleBtnTapped(_ sender: Any) { 55 | let actionSheet = UIAlertController(title: "Style", message: nil, preferredStyle: .actionSheet) 56 | KRTournamentViewStyle.allItems.forEach { style in 57 | actionSheet.addAction(UIAlertAction(title: style.str, style: .default) { [unowned self] _ in 58 | self.style = style 59 | }) 60 | } 61 | present(actionSheet, animated: true, completion: nil) 62 | } 63 | 64 | @IBAction func layerBtnTapped(_ sender: Any) { 65 | let actionSheet = UIAlertController(title: "Number of Layers", message: nil, preferredStyle: .actionSheet) 66 | (1..<5).forEach { index in 67 | actionSheet.addAction(UIAlertAction(title: "\(index)", style: .default) { [unowned self] _ in 68 | self.numberOfLayers = index 69 | }) 70 | } 71 | present(actionSheet, animated: true, completion: nil) 72 | } 73 | } 74 | 75 | // MARK: - KRTournament data source ------------------- 76 | 77 | extension MainVC: KRTournamentViewDataSource { 78 | func structure(of tournamentView: KRTournamentView) -> Bracket { 79 | return builder.build(format: true) 80 | } 81 | 82 | func tournamentView(_ tournamentView: KRTournamentView, entryAt index: Int) -> KRTournamentViewEntry { 83 | let entry = KRTournamentViewEntry() 84 | switch tournamentView.style { 85 | case .left, .right, .leftRight: 86 | entry.textLabel.text = entryNames[index] ?? "entry \(index+1)" 87 | case .top, .bottom, .topBottom: 88 | entry.textLabel.verticalText = entryNames[index] ?? "entry \(index+1)" 89 | } 90 | return entry 91 | } 92 | 93 | func tournamentView(_ tournamentView: KRTournamentView, matchAt matchPath: MatchPath) -> KRTournamentViewMatch { 94 | return MyMatch() 95 | } 96 | } 97 | 98 | // MARK: - KRTournament delegate ------------------- 99 | 100 | extension MainVC: KRTournamentViewDelegate { 101 | func tournamentView(_ tournamentView: KRTournamentView, didSelectEntryAt index: Int) { 102 | print("entry \(index) is selected") 103 | 104 | let alert = UIAlertController(title: "Change name", message: nil, preferredStyle: .alert) 105 | alert.addTextField(configurationHandler: nil) 106 | alert.addAction(UIAlertAction(title: "OK", style: .default) { [unowned self] _ in 107 | if let text = alert.textFields?.first?.text, text != "" { 108 | self.entryNames[index] = text 109 | } else { 110 | self.entryNames.removeValue(forKey: index) 111 | } 112 | }) 113 | present(alert, animated: true, completion: nil) 114 | } 115 | 116 | func tournamentView(_ tournamentView: KRTournamentView, didSelectMatchAt matchPath: MatchPath) { 117 | print("match \(matchPath.layer)-\(matchPath.item) is selected") 118 | guard let builder = builder.getChildBuilder(for: matchPath) else { return } 119 | guard let bracketVC = storyboard?.instantiateViewController(withIdentifier: "BracketVC") as? BracketVC else { return } 120 | bracketVC.matchPath = matchPath 121 | bracketVC.builder = builder 122 | bracketVC.delegate = self 123 | present(bracketVC, animated: false, completion: nil) 124 | } 125 | } 126 | 127 | // MARK: - BracketVC delegate ------------------- 128 | 129 | extension MainVC: BracketVCDelegate { 130 | func bracketVCDidClose(_ bracketVC: BracketVC) { 131 | tournamentView.reloadData() 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /DEMO/KRTournamentViewDemo/MyMatch.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyMatch.swift 3 | // KRTournamentViewDemo 4 | // 5 | // Copyright © 2019 Krimpedance. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | import KRTournamentView 10 | 11 | final class MyMatch: KRTournamentViewMatch { 12 | required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } 13 | 14 | init() { 15 | super.init(frame: .zero) 16 | 17 | backgroundColor = UIColor.green.withAlphaComponent(0.1) 18 | layer.borderColor = UIColor.green.withAlphaComponent(0.2).cgColor 19 | layer.borderWidth = 1 20 | 21 | let imageView = UIImageView(image: #imageLiteral(resourceName: "edit")) 22 | imageView.alpha = 0.8 23 | imageView.translatesAutoresizingMaskIntoConstraints = false 24 | addSubview(imageView) 25 | 26 | imageView.widthAnchor.constraint(equalToConstant: 20).isActive = true 27 | imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor).isActive = true 28 | imageView.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor).isActive = true 29 | imageView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor).isActive = true 30 | imageView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true 31 | imageView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /DEMO/KRTournamentViewDemo/StageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StageView.swift 3 | // KRTournamentViewDemo 4 | // 5 | // Copyright © 2019 Krimpedance. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | final class StageView: UIView { 11 | override func awakeFromNib() { 12 | super.awakeFromNib() 13 | backgroundColor = .white 14 | layer.shadowOffset = CGSize(width: 0, height: 2) 15 | layer.shadowOpacity = 0.5 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /DEMO/Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '11.0' 2 | 3 | target 'KRTournamentViewDemo' do 4 | # Comment the next line if you're not using Swift and don't want to use dynamic frameworks 5 | use_frameworks! 6 | 7 | pod 'KRTournamentView', path: '../KRTournamentView.podspec' 8 | 9 | end 10 | -------------------------------------------------------------------------------- /DEMO/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - KRTournamentView (1.1.0) 3 | 4 | DEPENDENCIES: 5 | - KRTournamentView (from `../KRTournamentView.podspec`) 6 | 7 | EXTERNAL SOURCES: 8 | KRTournamentView: 9 | :path: "../KRTournamentView.podspec" 10 | 11 | SPEC CHECKSUMS: 12 | KRTournamentView: c025fdc3b129178a3e9aa17cc6d2b001caeacd50 13 | 14 | PODFILE CHECKSUM: 6cb3926ce421603c4ace100c627ffbad8632bb10 15 | 16 | COCOAPODS: 1.8.4 17 | -------------------------------------------------------------------------------- /Documentation/En/Builder.md: -------------------------------------------------------------------------------- 1 | [日本語](../Ja/Builder.md) 2 | 3 | # Building a tournament structure 4 | 5 | - [Basic building](#basic-building) 6 | - [Entry](#entry) 7 | - [Bracket](#bracket) 8 | - [Building example](#building-example) 9 | - [Formatting](#formatting) 10 | - [MatchPath](#matchpath) 11 | - [Builder building](#builder-building) 12 | - [Initialization](#initialization) 13 | - [Building a structure](#building-a-structure) 14 | - [Build](#build) 15 | - [Building example](#building-example) 16 | 17 | # Basic building 18 | 19 | The tournament structure is a tree structure. 20 | 21 | The final game is the root node, and each entry is a leaf node. 22 | 23 | 24 | 25 | ## Entry 26 | 27 | In `KRTournamentView`, leaf nodes in tree structure are represented by `Entry` struct. 28 | 29 | ```swift 30 | let entry = Entry() 31 | ``` 32 | 33 | ## Bracket 34 | 35 | Nodes other than leaf nodes are represented by `Bracket` struct. 36 | 37 | ```swift 38 | let bracket = Bracket( 39 | children: [Entry(), Entry()], 40 | numberOfWinners: 1, 41 | winnerIndexes: [0] 42 | ) 43 | ``` 44 | 45 | + `children` 46 | 47 | Set child structures as an array. 48 | 49 | + `numberOfWinnders` 50 | 51 | Set the number of wins in the `Bracket`. (Default: 1) 52 | 53 | Use in situations such as "two players win in a four-player match". 54 | 55 | + `winnerIndexes` 56 | 57 | To set the winners, set the `children` indices as an array. 58 | 59 | ## Building example 60 | 61 | Here is an example of building a tournament structure using `Entry` and `Bracket`. 62 | 63 | 64 | 80 | 83 |
65 | 66 | ```swift 67 | let bracket = Bracket( 68 | children: [ 69 | Bracket( 70 | children: [Entry(), Entry(), Entry()], 71 | numberOfWinners: 2, 72 | winnerIndexes: [0, 2] 73 | ), 74 | Entry() 75 | ], 76 | winnerIndexes: [2] 77 | ) 78 | ``` 79 | 81 | 82 |
84 | 85 | ## Formatting 86 | 87 | Finally, you can set the index of the `Entry` (`Entry.index`) and the location of the `Bracket` (`Bracket.matchPath`) by formatting. 88 | 89 | **(In most cases, you do not need to do this, as `KRTournamentView` will automatically format it when it loads it.)** 90 | 91 | ```swift 92 | bracket.format() 93 | // or 94 | let formatted = bracket.formatted() 95 | ``` 96 | 97 | ### MatchPath 98 | 99 | This is a struct object composed of `layer` and `item`, and it represents the position of each `Bracket`. 100 | 101 | ```swift 102 | struct MatchPath { 103 | public let layer: Int 104 | public let item: Int 105 | } 106 | ``` 107 | 108 | The relationship `Entry.index` and `Bracket.matchPath` is as shown below. 109 | (Number at each intersection: `{layer}-{item}`) 110 | 111 | |`.left`|`.topBottom`| 112 | |:--:|:--:| 113 | ||| 114 | 115 | 116 | # Builder building 117 | 118 | To make building complex tournament structures easier, you can use the `TournamentBuilder` class. 119 | 120 | First, let's look at a simple building example. 121 | 122 | Compared to using `Entry` and` Bracket` directly, you can see that it can be built more concisely. 123 | 124 | 125 | 147 | 150 |
126 | 127 | ```swift 128 | // Use TournamentBuilder 129 | let builder = TournamentBuilder(numberOfLayers: 3) 130 | let bracket = builder.build() 131 | 132 | 133 | // Use Entry and Bracket 134 | let bracket = Bracket(children: [ 135 | Bracket(children: [ 136 | Bracket(children: [Entry(), Entry()]), 137 | Bracket(children: [Entry(), Entry()]) 138 | ]), 139 | Bracket(children: [ 140 | Bracket(children: [Entry(), Entry()]), 141 | Bracket(children: [Entry(), Entry()]) 142 | ]) 143 | ]) 144 | ``` 145 | 146 | 148 | 149 |
151 | 152 | ## Initialization 153 | 154 | `TournamentBuilder` can be initialized in the same way as` Bracket`. 155 | 156 | The difference is that `children` is an array of `BuildType` enum. 157 | 158 | ```swift 159 | let builder = TournamentBuilder( 160 | children: [.entry, .entry], 161 | numberOfWinners: 1, 162 | winnerIndexes: [0] 163 | ) 164 | ``` 165 | 166 | Also, if the structure is the same in all matches, you can initialize them collectively with the following initializers: 167 | 168 | 169 | 183 | 186 |
170 | 171 | ```swift 172 | let builder = TournamentBuilder( 173 | numberOfLayers: 2, // Number of layers in torunament. Height in tree structure. 174 | numberOfEntries: 3, 175 | numberOfWinners: 1 176 | ) { matchPath -> [Int] in 177 | // Returns winnerIndexes for each matchPath 178 | return (matchPath.layer == 1) ? [0] : [1] 179 | } 180 | ``` 181 | 182 | 184 | 185 |
187 | 188 | ## Building a structure 189 | 190 | In addition to directly setting the `childeren` property etc., it is also possible to build by method chain. 191 | 192 | ```swift 193 | func setNumberOfWinners(_ num: Int) -> Self 194 | func setWinnerIndexes(_ indexes: [Int]) -> Self 195 | func addEntry(_ num: Int = 1) -> Self 196 | func addBracket(_ handler: () -> TournamentBuilder) -> Self 197 | ``` 198 | 199 | ## Build 200 | 201 | Convert to `Bracket` by `build()` function. 202 | When the argument `format` of the `build()` function is set to` true`, internally calls `Bracket.format()`. 203 | 204 | ```swift 205 | let bracket = blacket.build() 206 | // or 207 | let bracket = blacket.build(format: true) 208 | ``` 209 | 210 | In addition, you can perform initialization and build together using the following static functions: 211 | 212 | ```swift 213 | let bracket = TournamentBuilder.build( 214 | numberOfLayers: 2, 215 | numberOfEntries: 3, 216 | numberOfWinners: 1 217 | ) { matchPath -> [Int] in 218 | return [] 219 | } 220 | 221 | // let builder = TournamentBuilder( 222 | // numberOfLayers: 2, 223 | // numberOfEntries: 3, 224 | // numberOfWinners: 1 225 | // ) { matchPath -> [Int] in 226 | // return [] 227 | // } 228 | // let bracket = builder.build(format: true) 229 | ``` 230 | 231 | ## Building example 232 | 233 | 234 | 255 | 258 |
235 | 236 | ```swift 237 | let bracket = TournamentBuilder() 238 | .addBracket({ 239 | TournamentBuilder() 240 | .setWinnerIndexes([0]) 241 | .addEntry() 242 | .addBracket { 243 | .init(children: [.entry, .entry], winnerIndexes: [0]) 244 | } 245 | }) 246 | .addBracket({ 247 | TournamentBuilder() 248 | .setNumberOfWinners(2) 249 | .addEntry(3) 250 | }) 251 | .build() 252 | ``` 253 | 254 | 256 | 257 |
259 | -------------------------------------------------------------------------------- /Documentation/En/HowToUse.md: -------------------------------------------------------------------------------- 1 | [日本語](../Ja/HowToUse.md) 2 | 3 | # How to use 4 | 5 | (see sample Xcode project in `/DEMO`) 6 | 7 | - [Add KRTournamentView](#add-krtournamentview) 8 | - [KRTournamentViewDataSource](#krtournamentviewdatasource) 9 | - [KRTournamentViewDelegate](#krtournamentviewdelegate) 10 | - [Reload the data](#reload-the-data) 11 | 12 | # Add KRTournamentView 13 | 14 | `KRTournamentView` controls presentation and behavior by `KRTournamentViewDataSource` and `KRTournamentViewDelegate` like a `UITableView`. 15 | 16 | ```swift 17 | let tournamentView = KRTournamentView() 18 | tournamentView.dataSource = self 19 | tournamentView.delegate = self 20 | addSubview(tournamentView) 21 | ``` 22 | 23 | # KRTournamentViewDataSource 24 | 25 | This protocol represents the data model object. 26 | 27 | ```swift 28 | protocol KRTournamentViewDataSource { 29 | // Structure of tournament bracket. 30 | func structure(of tournamentView: KRTournamentView) -> Bracket 31 | // Implements a view for each entry. 32 | func tournamentView(_ tournamentView: KRTournamentView,entryAt index: Int) -> KRTournamentViewEntry 33 | // Implements a view for each match. The winner can be set here. 34 | func tournamentView(_ tournamentView: KRTournamentView,matchAt matchPath: MatchPath) -> KRTournamentViewMatch 35 | } 36 | ``` 37 | 38 | + `structure(of tournamentView: KRTournamentView) -> Bracket` 39 | 40 | Returns tournament structure. 41 | 42 | Please refer [here](./Builder.md) for how to make a tournament structure. 43 | 44 | + `tournamentView(_ tournamentView: KRTournamentView,entryAt index: Int) -> KRTournamentViewEntry` 45 | 46 | This function repeats as many times as the number of entries. 47 | `index` is numbered from left to right, from top to bottom. 48 | 49 | You can create your own design by extending `KRTournamentViewEntry`. 50 | 51 | + `tournamentView(_ tournamentView: KRTournamentView,matchAt matchPath: MatchPath) -> KRTournamentViewMatch` 52 | 53 | This function repeats as many times as the number of games. 54 | See [here](./Builder.md#matchpath) for `MatchPath`. 55 | 56 | You can create your own design by extending `KRTournamentViewMatch`. 57 | 58 | 59 | # KRTournamentViewDelegate 60 | 61 | This protocol represents the entries size and behaviour of the entries and matches. 62 | 63 | ```swift 64 | protocol KRTournamentViewDelegate { 65 | // Returns entries size. 66 | func entrySize(in tournamentView: KRTournamentView) -> CGSize 67 | // Called after the user changes the selection of entry. 68 | func tournamentView(_ tournamentView: KRTournamentView,didSelectEntryAt index: Int) 69 | // Called after the user changes the selection of match. 70 | func tournamentView(_ tournamentView: KRTournamentView,didSelectMatchAt matchPath: MatchPath) 71 | } 72 | ``` 73 | 74 | + `entrySize(in tournamentView: KRTournamentView) -> CGSize` 75 | 76 | Please refer to [here](./Style.md#entry-size-setting) for the appearance due to the difference in size. 77 | 78 | 79 | # Reload the data 80 | 81 | To reload the data, use the following function. 82 | 83 | ```swift 84 | func reloadData() 85 | ``` -------------------------------------------------------------------------------- /Documentation/En/README.md: -------------------------------------------------------------------------------- 1 | [日本語](../Ja/README.md) 2 | 3 | # Documentation 4 | 5 | + [How to use](./HowToUse.md) 6 | + [Styles](./Style.md) 7 | + [Building a tournament structure](./Builder.md) -------------------------------------------------------------------------------- /Documentation/En/Style.md: -------------------------------------------------------------------------------- 1 | [日本語](../Ja/Style.md) 2 | 3 | # Styles 4 | 5 | - [The orientation of the tournament bracket](#the-orientation-of-the-tournament-bracket) 6 | - [color and line width](#color-and-line-width) 7 | - [Entry size setting](#entry-size-setting) 8 | - [Behavior by rotating the screen](#behavior-by-rotating-the-screen) 9 | 10 | # The orientation of the tournament bracket 11 | 12 | Set the orientation of the tournament bracket in the `style` property. 13 | 14 | |`.left`|`.right`|`.top`|`.bottom`|`.leftRight(direction: .top)`|`.topBottom(direction: .right)`| 15 | |:--:|:--:|:--:|:--:|:--:|:--:| 16 | ||||||| 17 | 18 | Set the direction of the finish match line to the `direction` property of `.leftRight` and `.topBottom`. 19 | 20 | + `.leftRight` : `.top` or `.bottom` 21 | + `.topBottom` : `.right` or `.left` 22 | 23 | 24 | # color and line width 25 | 26 | Customize with the following properties. 27 | 28 | ```swift 29 | var lineColor: UIColor // Line color (default is black). 30 | var winnerLinecolor: UIColor // Winner's line color (default is red). 31 | var lineWidth: CGFloat // Line width (default is 1.0). 32 | var winnerLineWidth: CGFloat? // Winner's line width (default is nil). In the case of nil, it is the same as lineWidth. 33 | ``` 34 | 35 | # Entry size setting 36 | 37 | By implementing [Delegate](./HowToUse.md#krtournamentviewdelegate), you can change the size of the entry. 38 | 39 | |80 x 30|200 x 80| 40 | |:--:|:--:| 41 | ||| 42 | 43 | # Behavior by rotating the screen 44 | 45 | ```swift 46 | var fixOrientation: Bool = false 47 | ``` 48 | 49 | When the above property is set to `true`, the tournament bracket will be in the same orientation as before rotation. 50 | 51 | However, the order of entries may change. 52 | (Entries are always in order from left to right, top to bottom.) 53 | 54 | |`.portrait`(initial)|`.landscapeLeft`|`.landscapeRight`|`.portraitUpsideDown`| 55 | |:--:|:--:|:--:|:--:| 56 | |`.left`|`.bottom`|`.top`|`.right`| 57 | |`.top`|`.left`|`.right`|`.bottom`| 58 | |`.leftRight(.top)`|`.topBottom(.left)`|`.topBottom(.right)`|`.leftRight(.bottom)`| -------------------------------------------------------------------------------- /Documentation/Ja/Builder.md: -------------------------------------------------------------------------------- 1 | [English](../En/Builder.md) 2 | 3 | # トーナメント構造の構築 4 | 5 | - [基本的な構築](#基本的な構築) 6 | - [Entry](#entry) 7 | - [Bracket](#bracket) 8 | - [構築例](#構築例) 9 | - [フォーマット](#フォーマット) 10 | - [MatchPath](#matchpath) 11 | - [ビルダーによる構築](#ビルダーによる構築) 12 | - [初期化](#初期化) 13 | - [トーナメント構造の構築](#トーナメント構造の構築) 14 | - [ビルド](#ビルド) 15 | - [構築例](#構築例) 16 | 17 | # 基本的な構築 18 | 19 | トーナメント構造は木構造とみることができます. 20 | 21 | 一般的に決勝戦となる部分が根ノードで,初戦の各参加者の部分がそれぞれ葉ノードとなります. 22 | 23 | 24 | 25 | ## Entry 26 | 27 | `KRTournamentView` では,葉ノードにあたる部分を `Entry` 構造体で表現します. 28 | 29 | ```swift 30 | let entry = Entry() 31 | ``` 32 | 33 | ## Bracket 34 | 35 | 葉ノード以外のノードに当たる部分は `Bracket` 構造体で表現します. 36 | 37 | ```swift 38 | let bracket = Bracket( 39 | children: [Entry(), Entry()], 40 | numberOfWinners: 1, 41 | winnerIndexes: [0] 42 | ) 43 | ``` 44 | 45 | + `children` 46 | 47 | 子ノードに当たる部分を配列で渡します. 48 | 49 | + `numberOfWinnders` 50 | 51 | その `Bracket` における勝利数を設定します.(初期値: 1) 52 | 53 | 「4人対戦の2人勝ち上がり」などの状況で使用します. 54 | 55 | + `winnerIndexes` 56 | 57 | 勝利者を設定する場合,`children` のインデックスを配列で渡します. 58 | 59 | ## 構築例 60 | 61 | `Entry`, `Bracket` を使用したトーナメント構造の構築例を示します. 62 | 63 | 64 | 80 | 83 |
65 | 66 | ```swift 67 | let bracket = Bracket( 68 | children: [ 69 | Bracket( 70 | children: [Entry(), Entry(), Entry()], 71 | numberOfWinners: 2, 72 | winnerIndexes: [0, 2] 73 | ), 74 | Entry() 75 | ], 76 | winnerIndexes: [2] 77 | ) 78 | ``` 79 | 81 | 82 |
84 | 85 | ## フォーマット 86 | 87 | 最後にフォーマットを行うことで,`Entry` の通し番号(`Entry.index`)と`Bracket`の位置情報(`Bracket.matchPath`)を設定することができます. 88 | 89 | **(`KRTournamentView` が読み込む時に自動でフォーマットをするので,大抵の場合は行う必要はありません.)** 90 | 91 | ```swift 92 | bracket.format() 93 | // もしくは 94 | let formatted = bracket.formatted() 95 | ``` 96 | 97 | ### MatchPath 98 | 99 | `MatchPath` は,`layer` と `item` からなる構造体で, `Bracket` の位置を表しています. 100 | 101 | ```swift 102 | struct MatchPath { 103 | public let layer: Int 104 | public let item: Int 105 | } 106 | ``` 107 | 108 | `Entry.index` と `Bracket.matchPath` の関係は以下のようになります. 109 | (各交点の数字: `{layer}-{item}`) 110 | 111 | |`.left`|`.topBottom`| 112 | |:--:|:--:| 113 | ||| 114 | 115 | 116 | # ビルダーによる構築 117 | 118 | 複雑なトーナメント表をより簡単に構築するには,`TournamentBuilder` クラスを利用できます. 119 | 120 | まず,簡単な構築例をみてみましょう. 121 | 122 | `Entry`, `Bracket` を直接使用した場合と比べると,簡潔に構築できることが分かります. 123 | 124 | 125 | 147 | 150 |
126 | 127 | ```swift 128 | // TournamentBuilder による構築 129 | let builder = TournamentBuilder(numberOfLayers: 3) 130 | let bracket = builder.build() 131 | 132 | 133 | // Entry, Bracket による構築 134 | let bracket = Bracket(children: [ 135 | Bracket(children: [ 136 | Bracket(children: [Entry(), Entry()]), 137 | Bracket(children: [Entry(), Entry()]) 138 | ]), 139 | Bracket(children: [ 140 | Bracket(children: [Entry(), Entry()]), 141 | Bracket(children: [Entry(), Entry()]) 142 | ]) 143 | ]) 144 | ``` 145 | 146 | 148 | 149 |
151 | 152 | ## 初期化 153 | 154 | `TournamentBuilder` は `Bracket` と同じような初期化ができます. 155 | 156 | 違う点は,`children` が,列挙型の `BuildType` の配列になっているところです. 157 | 158 | ```swift 159 | let builder = TournamentBuilder( 160 | children: [.entry, .entry], 161 | numberOfWinners: 1, 162 | winnerIndexes: [0] 163 | ) 164 | ``` 165 | 166 | また,全ての対戦において構造が同じ場合は,以下のイニシャライザでまとめて初期化できます. 167 | 168 | 169 | 183 | 186 |
170 | 171 | ```swift 172 | let builder = TournamentBuilder( 173 | numberOfLayers: 2, // トーナメントのレイヤー数. 木構造でいうところの高さ. 174 | numberOfEntries: 3, 175 | numberOfWinners: 1 176 | ) { matchPath -> [Int] in 177 | // matchPath ごとに winnerIndexes を返す 178 | return (matchPath.layer == 1) ? [0] : [1] 179 | } 180 | ``` 181 | 182 | 184 | 185 |
187 | 188 | ## トーナメント構造の構築 189 | 190 | `childeren` プロパティなどを直接設定して構築する他に,メソッドチェーンによる構築も可能です. 191 | 192 | ```swift 193 | func setNumberOfWinners(_ num: Int) -> Self 194 | func setWinnerIndexes(_ indexes: [Int]) -> Self 195 | func addEntry(_ num: Int = 1) -> Self 196 | func addBracket(_ handler: () -> TournamentBuilder) -> Self 197 | ``` 198 | 199 | ## ビルド 200 | 201 | `build()` 関数により, `Bracket` へ変換します. 202 | `build()` 関数の引数 `format` に `true` を渡すことで,`Bracket.format()` も行います. 203 | 204 | ```swift 205 | let bracket = blacket.build() 206 | // または 207 | let bracket = blacket.build(format: true) 208 | ``` 209 | 210 | また,以下のスタティック関数を利用することで,初期化とビルドを同時に行うことも可能です. 211 | 212 | ```swift 213 | let bracket = TournamentBuilder.build( 214 | numberOfLayers: 2, 215 | numberOfEntries: 3, 216 | numberOfWinners: 1 217 | ) { matchPath -> [Int] in 218 | return [] 219 | } 220 | 221 | // 以下と同じ 222 | // let builder = TournamentBuilder( 223 | // numberOfLayers: 2, 224 | // numberOfEntries: 3, 225 | // numberOfWinners: 1 226 | // ) { matchPath -> [Int] in 227 | // return [] 228 | // } 229 | // let bracket = builder.build(format: true) 230 | ``` 231 | 232 | ## 構築例 233 | 234 | 235 | 256 | 259 |
236 | 237 | ```swift 238 | let bracket = TournamentBuilder() 239 | .addBracket({ 240 | TournamentBuilder() 241 | .setWinnerIndexes([0]) 242 | .addEntry() 243 | .addBracket { 244 | .init(children: [.entry, .entry], winnerIndexes: [0]) 245 | } 246 | }) 247 | .addBracket({ 248 | TournamentBuilder() 249 | .setNumberOfWinners(2) 250 | .addEntry(3) 251 | }) 252 | .build() 253 | ``` 254 | 255 | 257 | 258 |
260 | -------------------------------------------------------------------------------- /Documentation/Ja/HowToUse.md: -------------------------------------------------------------------------------- 1 | [English](../En/HowToUse.md) 2 | 3 | # 使い方 4 | 5 | `/Demo` フォルダにサンプルがあるのでそちらも参考にしてください. 6 | 7 | - [トーナメント表の追加](#トーナメント表の追加) 8 | - [KRTournamentViewDataSource](#krtournamentviewdatasource) 9 | - [KRTournamentViewDelegate](#krtournamentviewdelegate) 10 | - [データの再読み込み](#データの再読み込み) 11 | 12 | # トーナメント表の追加 13 | 14 | `KRTournamentView` は,`UITableView` のように`KRTournamentViewDataSource` と `KRTournamentViewDelegate` により表示と挙動を制御します. 15 | 16 | ```swift 17 | let tournamentView = KRTournamentView() 18 | tournamentView.dataSource = self 19 | tournamentView.delegate = self 20 | addSubview(tournamentView) 21 | ``` 22 | 23 | # KRTournamentViewDataSource 24 | 25 | このプロトコルを用いて,トーナメント構造と表示データを実装します. 26 | 27 | ```swift 28 | protocol KRTournamentViewDataSource { 29 | // トーナメント表の構造を返すようにします. 30 | func structure(of tournamentView: KRTournamentView) -> Bracket 31 | // 各Entryのビューを実装します. 32 | func tournamentView(_ tournamentView: KRTournamentView,entryAt index: Int) -> KRTournamentViewEntry 33 | // 各Matchのビューを実装します. 勝者はここで設定できます. 34 | func tournamentView(_ tournamentView: KRTournamentView,matchAt matchPath: MatchPath) -> KRTournamentViewMatch 35 | } 36 | ``` 37 | 38 | + `structure(of tournamentView: KRTournamentView) -> Bracket` 39 | 40 | トーナメント構造を返します. 41 | 構築方法は[こちら](./Builder.md)を参考にしてください. 42 | 43 | + `tournamentView(_ tournamentView: KRTournamentView,entryAt index: Int) -> KRTournamentViewEntry` 44 | 45 | この関数はエントリーの数だけ呼ばれます. 46 | `index` は左から右,上から下の順に採番されます. 47 | 48 | `KRTournamentViewEntry` クラスを拡張することで,独自のデザインを行えます. 49 | 50 | + `tournamentView(_ tournamentView: KRTournamentView,matchAt matchPath: MatchPath) -> KRTournamentViewMatch` 51 | 52 | この関数は対戦の数だけ呼ばれます. 53 | `MatchPath` については[こちら](./Builder.md#matchpath)を参照してください. 54 | 55 | `KRTournamentViewMatch` クラスを拡張することで,独自のデザインを行えます. 56 | 57 | 58 | # KRTournamentViewDelegate 59 | 60 | このプロトコルを用いて,エントリーのサイズやタップ時の挙動を実装します. 61 | 62 | ```swift 63 | protocol KRTournamentViewDelegate { 64 | // Entry のサイズを返します. 65 | func entrySize(in tournamentView: KRTournamentView) -> CGSize 66 | // Entry が選択された時に呼ばれます. 67 | func tournamentView(_ tournamentView: KRTournamentView,didSelectEntryAt index: Int) 68 | // Match が選択された時に呼ばれます. 69 | func tournamentView(_ tournamentView: KRTournamentView,didSelectMatchAt matchPath: MatchPath) 70 | } 71 | ``` 72 | 73 | + `entrySize(in tournamentView: KRTournamentView) -> CGSize` 74 | 75 | サイズの違いによる見た目は[こちら](./Style.md#エントリーのサイズ設定)を参考にしてください. 76 | 77 | 78 | # データの再読み込み 79 | 80 | データを再読み込みする場合は,以下の関数を呼びます. 81 | 82 | ```swift 83 | func reloadData() 84 | ``` -------------------------------------------------------------------------------- /Documentation/Ja/README.md: -------------------------------------------------------------------------------- 1 | [English](../En/README.md) 2 | 3 | # ドキュメント 4 | 5 | + [使い方](./HowToUse.md) 6 | + [トーナメント表の見た目の設定](./Style.md) 7 | + [トーナメント構造の構築方法](./Builder.md) -------------------------------------------------------------------------------- /Documentation/Ja/Style.md: -------------------------------------------------------------------------------- 1 | [English](../En/Style.md) 2 | 3 | # トーナメント表の見た目の設定 4 | 5 | - [向きの設定](#向きの設定) 6 | - [色, 線幅の設定](#色-線幅の設定) 7 | - [エントリーのサイズ設定](#エントリーのサイズ設定) 8 | - [画面の回転による挙動の変更](#画面の回転による挙動の変更) 9 | 10 | # 向きの設定 11 | 12 | `style` プロパティによりトーナメント表の向きを設定します. 13 | 14 | |`.left`|`.right`|`.top`|`.bottom`|`.leftRight(direction: .top)`|`.topBottom(direction: .right)`| 15 | |:--:|:--:|:--:|:--:|:--:|:--:| 16 | ||||||| 17 | 18 | `.leftRight` と `.topBottom` の `direction` プロパティには, 決勝戦の線の向きを設定します. 19 | 20 | + `.leftRight` : `.top` または `.bottom` 21 | + `.topBottom` : `.right` または `.left` 22 | 23 | 24 | # 色, 線幅の設定 25 | 26 | 以下のプロパティによりカスタマイズできます. 27 | 28 | ```swift 29 | var lineColor: UIColor // 線の色(初期値は黒). 30 | var winnerLinecolor: UIColor // 勝者の線の色(初期値は赤). 31 | var lineWidth: CGFloat // 線の幅(初期値は1.0). 32 | var winnerLineWidth: CGFloat? // 勝者の線の幅(初期値はnil). nilの場合は lineWidth と同じになる. 33 | ``` 34 | 35 | # エントリーのサイズ設定 36 | 37 | [デリゲート](./HowToUse.md#KRTournamentViewDelegate) を実装することで,エントリーのサイズを変更できます. 38 | 39 | |80 x 30|200 x 80| 40 | |:--:|:--:| 41 | ||| 42 | 43 | # 画面の回転による挙動の変更 44 | 45 | ```swift 46 | var fixOrientation: Bool = false 47 | ``` 48 | 49 | このプロパティを `true` にすると, 画面回転時にトーナメント表は回転前と同じ向きになります. 50 | 51 | (エントリーは常に左から右, 上から下の順になるため,エントリーの順番が逆になることがあります.) 52 | 53 | |`.portrait`(初期)|`.landscapeLeft`|`.landscapeRight`|`.portraitUpsideDown`| 54 | |:--:|:--:|:--:|:--:| 55 | |`.left`|`.bottom`|`.top`|`.right`| 56 | |`.top`|`.left`|`.right`|`.bottom`| 57 | |`.leftRight(.top)`|`.topBottom(.left)`|`.topBottom(.right)`|`.leftRight(.bottom)`| -------------------------------------------------------------------------------- /KRTournamentView.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "KRTournamentView" 3 | s.version = "2.0.0" 4 | s.summary = "A flexible tournament bracket." 5 | s.description = "KRTournamentView is a flexible tournament bracket that can respond to the various structure on iOS." 6 | s.homepage = "https://github.com/krimpedance/KRTournamentView" 7 | s.license = { :type => "MIT", :file => "LICENSE" } 8 | 9 | s.author = { "krimpedance" => "info@krimpedance.com" } 10 | s.requires_arc = true 11 | 12 | s.ios.deployment_target = '8.0' 13 | 14 | s.source = { :git => "https://github.com/krimpedance/KRTournamentView.git", :tag => s.version.to_s } 15 | s.source_files = "KRTournamentView/**/*.swift" 16 | end 17 | -------------------------------------------------------------------------------- /KRTournamentView.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /KRTournamentView.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /KRTournamentView.xcodeproj/xcshareddata/xcschemes/KRTournamentView.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 50 | 51 | 52 | 53 | 54 | 55 | 65 | 66 | 72 | 73 | 74 | 75 | 76 | 77 | 83 | 84 | 90 | 91 | 92 | 93 | 95 | 96 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /KRTournamentView/Classes/KRTournamentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KRTournamentView.swift 3 | // KRTournamentView 4 | // 5 | // Copyright © 2018 Krimpedance. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | /// KRTournamentView is a flexible tournament bracket which can support to the various structure. 11 | @IBDesignable open class KRTournamentView: UIView, KRTournamentViewDataStore { 12 | private lazy var drawingView = KRTournamentDrawingView(dataStore: self) 13 | private lazy var firstEntriesView = KRTournamentEntriesView(dataStore: self, drawHalf: .first) 14 | private lazy var secondEntriesView = KRTournamentEntriesView(dataStore: self, drawHalf: .second) 15 | private var orientation = UIDeviceOrientation.portrait 16 | private var orientationObserver: NSObjectProtocol? 17 | 18 | /// KRTournamentView style. Default is `.left`. 19 | open var style: KRTournamentViewStyle = Default.style 20 | 21 | /// Structure of tournament. This is set by the data source. 22 | internal(set) public var tournamentStructure: Bracket = Default.tournamentStructure 23 | 24 | /// Entry size. This is set by the delegate. Default is CGSize(80.0, 30.0). 25 | internal(set) public var entrySize: CGSize = Default.entrySize(with: Default.style) 26 | 27 | /// KRTournamentView data source. 28 | open weak var dataSource: KRTournamentViewDataSource? 29 | 30 | /// KRTournamentView delegate. 31 | open weak var delegate: KRTournamentViewDelegate? 32 | 33 | /// Line color. 34 | @IBInspectable public var lineColor: UIColor = Default.lineColor 35 | 36 | /// Winner's line color. 37 | @IBInspectable public var winnerLineColor: UIColor = Default.winnerLineColor 38 | 39 | /// Line width. 40 | @IBInspectable public var lineWidth: CGFloat = Default.lineWidth 41 | 42 | /// winner's line width. If nil, `lineWidth` is used. 43 | public var winnerLineWidth: CGFloat? 44 | 45 | /// If true, the tournament direction isn't changed when the device is rotated. 46 | @IBInspectable public var fixOrientation: Bool = Default.fixOrientation 47 | 48 | // Lifecycle ------------ 49 | 50 | open override func layoutSubviews() { 51 | super.layoutSubviews() 52 | updateLayout() 53 | reloadData() 54 | 55 | if orientationObserver != nil { return } 56 | 57 | orientationObserver = NotificationCenter.default.addObserver(forName: UIDevice.orientationDidChangeNotification, object: nil, queue: .main) { [weak self] _ in 58 | self?.checkRotation() 59 | } 60 | } 61 | } 62 | 63 | // MARK: - Private layout actions ------------ 64 | 65 | private extension KRTournamentView { 66 | func updateLayout() { 67 | entrySize = delegate?.entrySize(in: self) ?? Default.entrySize(with: style) 68 | removeConstraints() 69 | 70 | addSubview(drawingView) 71 | addSubview(firstEntriesView) 72 | addSubview(secondEntriesView) 73 | 74 | let sizeAttribute: NSLayoutConstraint.Attribute = style.isVertical ? .width : .height 75 | let firstAttribute: NSLayoutConstraint.Attribute = style.isVertical ? .left : .top 76 | let secondAttribute: NSLayoutConstraint.Attribute = style.isVertical ? .right : .bottom 77 | let commonAttributes: [NSLayoutConstraint.Attribute] = style.isVertical ? [.top, .bottom] : [.left, .right] 78 | 79 | addConstraints( 80 | NSLayoutConstraint.constraints(from: firstEntriesView, to: self, attributes: commonAttributes + [firstAttribute]) + 81 | NSLayoutConstraint.constraints(from: secondEntriesView, to: self, attributes: commonAttributes + [secondAttribute]) + 82 | NSLayoutConstraint.constraints(from: drawingView, to: self, attributes: commonAttributes) + 83 | [ 84 | NSLayoutConstraint(item: drawingView, attribute: firstAttribute, toItem: firstEntriesView, attribute: secondAttribute, constant: 5), 85 | NSLayoutConstraint(item: secondEntriesView, attribute: firstAttribute, toItem: drawingView, attribute: secondAttribute, constant: 5) 86 | ] 87 | ) 88 | 89 | let entriesSpaceWidth = style.isVertical ? entrySize.width : entrySize.height 90 | var (firstConstant, secondConstant) = (entriesSpaceWidth, entriesSpaceWidth) 91 | switch style { 92 | case .left, .top: secondConstant = 0 93 | case .right, .bottom: firstConstant = 0 94 | case .leftRight, .topBottom: break 95 | } 96 | firstEntriesView.addConstraint(NSLayoutConstraint(item: firstEntriesView, attribute: sizeAttribute, constant: firstConstant)) 97 | secondEntriesView.addConstraint(NSLayoutConstraint(item: secondEntriesView, attribute: sizeAttribute, constant: secondConstant)) 98 | } 99 | 100 | func removeConstraints() { 101 | drawingView.removeFromSuperview() 102 | firstEntriesView.removeFromSuperview() 103 | secondEntriesView.removeFromSuperview() 104 | 105 | drawingView.removeConstraints(drawingView.constraints) 106 | firstEntriesView.removeConstraints(firstEntriesView.constraints) 107 | secondEntriesView.removeConstraints(secondEntriesView.constraints) 108 | } 109 | 110 | func checkRotation() { 111 | let orientation = UIDevice.current.orientation 112 | if !fixOrientation || self.orientation == orientation { return } 113 | 114 | switch (self.orientation, orientation) { 115 | case (.portrait, .landscapeLeft), (.landscapeLeft, .portraitUpsideDown), 116 | (.landscapeRight, .portrait), (.portraitUpsideDown, .landscapeRight): 117 | style.rotateLeft() 118 | 119 | case (.portrait, .landscapeRight), (.landscapeLeft, .portrait), 120 | (.landscapeRight, .portraitUpsideDown), (.portraitUpsideDown, .landscapeLeft): 121 | style.rotateRight() 122 | 123 | case (.portrait, .portraitUpsideDown), (.landscapeLeft, .landscapeRight), 124 | (.landscapeRight, .landscapeLeft), (.portraitUpsideDown, .portrait): 125 | style.reverse() 126 | 127 | default: break 128 | } 129 | 130 | guard [.portrait, .landscapeLeft, .landscapeRight, .portraitUpsideDown].contains(orientation) else { return } 131 | 132 | self.orientation = orientation 133 | setNeedsLayout() 134 | } 135 | } 136 | 137 | // MARK: - Private actions ------------ 138 | 139 | private extension KRTournamentView { 140 | func reloadEntries() { 141 | func getView(entry: Entry) -> KRTournamentViewEntry { 142 | let view = (dataSource ?? self).tournamentView(self, entryAt: entry.index) 143 | view.index = entry.index 144 | view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didSelectEntry(sender:)))) 145 | return view 146 | } 147 | 148 | firstEntriesView.entries = tournamentStructure.entries(style: style, drawHalf: .first).map(getView) 149 | secondEntriesView.entries = tournamentStructure.entries(style: style, drawHalf: .second).map(getView) 150 | } 151 | 152 | func reloadMatches() { 153 | drawingView.matches = tournamentStructure.getMatchPaths().map { matchPath in 154 | let match = (dataSource ?? self).tournamentView(self, matchAt: matchPath) 155 | match.matchPath = matchPath 156 | match.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didSelectMatch(sender:)))) 157 | return match 158 | } 159 | } 160 | 161 | @objc func didSelectEntry(sender: UITapGestureRecognizer) { 162 | guard let entry = sender.view as? KRTournamentViewEntry else { return } 163 | delegate?.tournamentView(self, didSelectEntryAt: entry.index) 164 | } 165 | 166 | @objc func didSelectMatch(sender: UITapGestureRecognizer) { 167 | guard let match = sender.view as? KRTournamentViewMatch else { return } 168 | delegate?.tournamentView(self, didSelectMatchAt: match.matchPath) 169 | } 170 | } 171 | 172 | // MARK: - Open actions ------------ 173 | 174 | extension KRTournamentView { 175 | /// Reloads everything from scratch. Redisplays entries and matches. 176 | open func reloadData() { 177 | tournamentStructure = (dataSource ?? self).structure(of: self).formatted(force: false) 178 | reloadEntries() 179 | reloadMatches() 180 | setNeedsLayout() 181 | drawingView.setNeedsDisplay() 182 | } 183 | 184 | /// Returns entry for index. If index is out of range, returns nil. 185 | /// 186 | /// - Parameter index: index of entry. 187 | /// - Returns: KRTournamentViewEntry instance if exists. 188 | open func entry(at index: Int) -> KRTournamentViewEntry? { 189 | return firstEntriesView.entries.first { $0.index == index } ?? secondEntriesView.entries.first { $0.index == index } 190 | } 191 | 192 | /// Returns match for `MatchPath`. If matchPath is out of range, returns nil. 193 | /// 194 | /// - Parameter matchPath: `MatchPath` of match. 195 | /// - Returns: KRTournamentViewMatch instance if exists. 196 | open func match(at matchPath: MatchPath) -> KRTournamentViewMatch? { 197 | return drawingView.matches.first { $0.matchPath == matchPath } 198 | } 199 | } 200 | 201 | // MARK: - KRTournamentView data source ------------ 202 | 203 | extension KRTournamentView: KRTournamentViewDataSource { 204 | open func structure(of tournamentView: KRTournamentView) -> Bracket { 205 | return Default.tournamentStructure 206 | } 207 | 208 | open func tournamentView(_ tournamentView: KRTournamentView, entryAt index: Int) -> KRTournamentViewEntry { 209 | return Default.tournamentViewEntry 210 | } 211 | 212 | open func tournamentView(_ tournamentView: KRTournamentView, matchAt matchPath: MatchPath) -> KRTournamentViewMatch { 213 | return Default.tournamentViewMatch 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /KRTournamentView/Classes/KRTournamentViewDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KRTournamentViewDataSource.swift 3 | // KRTournamentView 4 | // 5 | // Copyright © 2018 Krimpedance. All rights reserved. 6 | // 7 | 8 | /// This protocol represents the data model object. as such, it supplies no information about appearance (including the entries and matches) 9 | public protocol KRTournamentViewDataSource: class { 10 | /// Structure of tournament bracket. 11 | /// 12 | /// - Parameter tournamentView: The tournament view. 13 | /// - Returns: Bracket you need. 14 | func structure(of tournamentView: KRTournamentView) -> Bracket 15 | 16 | /// Entry display. 17 | /// 18 | /// - Parameters: 19 | /// - tournamentView: The tournament view. 20 | /// - index: Entry index. 21 | /// - Returns: KRTournamentViewEntry instance. 22 | func tournamentView(_ tournamentView: KRTournamentView, entryAt index: Int) -> KRTournamentViewEntry 23 | 24 | /// Match display. 25 | /// 26 | /// - Parameters: 27 | /// - tournamentView: The tournament view. 28 | /// - matchPath: layer and number of the match. 29 | /// - Returns: KRTournamentViewMatch instance. 30 | func tournamentView(_ tournamentView: KRTournamentView, matchAt matchPath: MatchPath) -> KRTournamentViewMatch 31 | } 32 | -------------------------------------------------------------------------------- /KRTournamentView/Classes/KRTournamentViewDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KRTournamentViewDelegate.swift 3 | // KRTournamentView 4 | // 5 | // Copyright © 2018 Krimpedance. All rights reserved. 6 | // 7 | 8 | import CoreGraphics 9 | 10 | /// This represents the behaviour of the entries and matches. 11 | public protocol KRTournamentViewDelegate: class { 12 | // MARK: Layout ------------ 13 | 14 | /// Entry size. 15 | /// 16 | /// - Parameter tournamentView: the tournament view. 17 | /// - Returns: CGSize. 18 | func entrySize(in tournamentView: KRTournamentView) -> CGSize 19 | 20 | // MARK: Selection ------------ 21 | 22 | /// Called after the user changes the selection of entry. 23 | /// 24 | /// - Parameters: 25 | /// - tournamentView: The tournament view. 26 | /// - index: Index of selected entry. 27 | func tournamentView(_ tournamentView: KRTournamentView, didSelectEntryAt index: Int) 28 | 29 | /// Called after the user changes the selection of match. 30 | /// 31 | /// - Parameters: 32 | /// - tournamentView: The tournament view. 33 | /// - matchPath: MatchPath of selected match. 34 | func tournamentView(_ tournamentView: KRTournamentView, didSelectMatchAt matchPath: MatchPath) 35 | } 36 | 37 | // MARK: - Default behavior ------------ 38 | 39 | public extension KRTournamentViewDelegate { 40 | func entrySize(in tournamentView: KRTournamentView) -> CGSize { 41 | return Default.entrySize(with: tournamentView.style) 42 | } 43 | 44 | func tournamentView(_ tournamentView: KRTournamentView, didSelectEntryAt index: Int) {} 45 | func tournamentView(_ tournamentView: KRTournamentView, didSelectMatchAt matchPath: MatchPath) {} 46 | } 47 | -------------------------------------------------------------------------------- /KRTournamentView/Classes/KRTournamentViewEntry.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KRTournamentViewEntry.swift 3 | // KRTournamentView 4 | // 5 | // Copyright © 2018 Krimpedance. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | /// KRTournamentViewEntry is a view for entry of KRTournamentView 11 | open class KRTournamentViewEntry: UIView { 12 | /// Index in tournament view. 13 | internal(set) public var index: Int! 14 | 15 | /// Default is nil. Label will be created if necessary. 16 | private(set) open lazy var textLabel: KRTournamentViewEntryLabel = self.makeTextLabel() 17 | 18 | /// Initializer 19 | public convenience init() { 20 | self.init(frame: CGRect(origin: .zero, size: Default.entrySize(with: Default.style))) 21 | backgroundColor = .clear 22 | clipsToBounds = true 23 | } 24 | } 25 | 26 | // MARK: - Private actions ------------ 27 | 28 | private extension KRTournamentViewEntry { 29 | func makeTextLabel() -> KRTournamentViewEntryLabel { 30 | let label = KRTournamentViewEntryLabel() 31 | label.backgroundColor = .clear 32 | label.textAlignment = .center 33 | label.numberOfLines = 0 34 | label.font = UIFont.systemFont(ofSize: 15) 35 | label.adjustsFontSizeToFitWidth = true 36 | label.translatesAutoresizingMaskIntoConstraints = false 37 | addSubview(label) 38 | 39 | addConstraints([ 40 | NSLayoutConstraint(item: label, attribute: .top, toItem: self), 41 | NSLayoutConstraint(item: label, attribute: .bottom, toItem: self), 42 | NSLayoutConstraint(item: label, attribute: .left, toItem: self), 43 | NSLayoutConstraint(item: label, attribute: .right, toItem: self) 44 | ]) 45 | 46 | return label 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /KRTournamentView/Classes/KRTournamentViewEntryLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KRTournamentViewEntryLabel.swift 3 | // KRTournamentView 4 | // 5 | // Copyright © 2018 Krimpedance. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | /// KRTournamentViewEntryLabel is a label which expanded UILabel for vertical Text. 11 | public class KRTournamentViewEntryLabel: UILabel { 12 | /// Sets text to be written vertically. This is just insert new line each character. 13 | public var verticalText: String? { 14 | willSet { 15 | guard let text = newValue else { return } 16 | self.text = text.map { $0 }.reduce("", { (result, char) -> String in return "\(result)\(char)\n" }) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /KRTournamentView/Classes/KRTournamentViewMatch.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KRTournamentViewMatch.swift 3 | // KRTournamentView 4 | // 5 | // Copyright © 2018 Krimpedance. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | /// KRTournamentViewMatch is a view for match of KRTournamentView 11 | open class KRTournamentViewMatch: UIView { 12 | /// MatchPath in tournament view. 13 | internal(set) public var matchPath: MatchPath! 14 | 15 | /// Vertexes of winning line 16 | internal(set) public var winnerPoints: [CGPoint]! 17 | 18 | /// Initializer 19 | public convenience init() { 20 | self.init(frame: CGRect(x: 0, y: 0, width: 50, height: 50)) 21 | backgroundColor = .clear 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /KRTournamentView/Classes/KRTournamentViewStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KRTournamentViewStyle.swift 3 | // KRTournamentView 4 | // 5 | // Copyright © 2018 Krimpedance. All rights reserved. 6 | // 7 | 8 | /// Direction of KRTournamentView. 9 | /// 10 | /// - left: Entries are placed on the left. 11 | /// - right: Entries are placed on the right. 12 | /// - top: Entries are placed on the top. 13 | /// - bottom: Entries are placed on the bottom. 14 | /// - leftRight: Entries are placed on the left and right. 15 | /// - topBottom: Entries are placed on the top and bottom. 16 | public enum KRTournamentViewStyle { 17 | /// Direction of line of final match's winner on both side styles (.leftRight, .topBottom). 18 | public enum FinalDirection: Equatable { case top, bottom, left, right } 19 | 20 | case left, right, top, bottom 21 | case leftRight(direction: FinalDirection), topBottom(direction: FinalDirection) 22 | 23 | /// Alias for .leftRight(direction: .top) 24 | public static let leftRight = KRTournamentViewStyle.leftRight(direction: .top) 25 | 26 | /// Alias for .topBottom(direction: .right) 27 | public static let topBottom = KRTournamentViewStyle.topBottom(direction: .right) 28 | } 29 | 30 | // MARK: - Equatable ------------ 31 | 32 | extension KRTournamentViewStyle: Equatable { 33 | public static func == (lhs: KRTournamentViewStyle, rhs: KRTournamentViewStyle) -> Bool { 34 | switch (lhs, rhs) { 35 | case (.left, .left), (.right, .right), (.top, .top), (.bottom, .bottom): 36 | return true 37 | case (.leftRight(let lDirection), .leftRight(let rDirection)), (.topBottom(let lDirection), .topBottom(let rDirection)): 38 | return lDirection == rDirection 39 | default: return false 40 | } 41 | } 42 | } 43 | 44 | // MARK: - Internal computed property ------------ 45 | 46 | extension KRTournamentViewStyle { 47 | var direction: FinalDirection { 48 | switch self { 49 | case .left: return .right 50 | case .right: return .left 51 | case .top: return .bottom 52 | case .bottom: return .top 53 | case .leftRight(let direction): 54 | return (direction == .bottom) ? .bottom : .top 55 | case .topBottom(let direction): 56 | return (direction == .left) ? .left : .right 57 | } 58 | } 59 | 60 | var isVertical: Bool { 61 | switch self { 62 | case .left, .right, .leftRight: return true 63 | case .top, .bottom, .topBottom: return false 64 | } 65 | } 66 | 67 | var isHalf: Bool { 68 | switch self { 69 | case .left, .right, .top, .bottom: return true 70 | case .leftRight, .topBottom: return false 71 | } 72 | } 73 | } 74 | 75 | // MARK: - Internal action ------------ 76 | 77 | extension KRTournamentViewStyle { 78 | mutating func rotateLeft() { 79 | switch self { 80 | case .left: self = .bottom 81 | case .right: self = .top 82 | case .top: self = .left 83 | case .bottom: self = .right 84 | 85 | case .leftRight(let direction) where direction == .bottom: 86 | self = .topBottom(direction: .right) 87 | 88 | case .leftRight: 89 | self = .topBottom(direction: .left) 90 | 91 | case .topBottom(let direction) where direction == .left: 92 | self = .leftRight(direction: .bottom) 93 | 94 | case .topBottom: 95 | self = .leftRight(direction: .top) 96 | } 97 | } 98 | 99 | mutating func rotateRight() { 100 | switch self { 101 | case .left: self = .top 102 | case .right: self = .bottom 103 | case .top: self = .right 104 | case .bottom: self = .left 105 | 106 | case .leftRight(let direction) where direction == .bottom: 107 | self = .topBottom(direction: .left) 108 | 109 | case .leftRight: 110 | self = .topBottom(direction: .right) 111 | 112 | case .topBottom(let direction) where direction == .left: 113 | self = .leftRight(direction: .top) 114 | 115 | case .topBottom: 116 | self = .leftRight(direction: .bottom) 117 | } 118 | } 119 | 120 | mutating func reverse() { 121 | switch self { 122 | case .left: self = .right 123 | case .right: self = .left 124 | case .top: self = .bottom 125 | case .bottom: self = .top 126 | 127 | case .leftRight(let direction) where direction == .bottom: 128 | self = .leftRight(direction: .top) 129 | 130 | case .leftRight: 131 | self = .leftRight(direction: .bottom) 132 | 133 | case .topBottom(let direction) where direction == .left: 134 | self = .topBottom(direction: .right) 135 | 136 | case .topBottom: 137 | self = .topBottom(direction: .left) 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /KRTournamentView/Classes/MatchPath.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MatchPath.swift 3 | // KRTournamentView 4 | // 5 | // Copyright © 2018 Krimpedance. All rights reserved. 6 | // 7 | 8 | /// `MatchPath` represents the path to a specific match in a tournament. 9 | public struct MatchPath { 10 | public let layer: Int 11 | public let item: Int 12 | 13 | public init(layer: Int, item: Int) { 14 | self.layer = layer 15 | self.item = item 16 | } 17 | } 18 | 19 | // MARK: - Equatable ------------ 20 | 21 | extension MatchPath: Equatable { 22 | public static func == (lhs: MatchPath, rhs: MatchPath) -> Bool { 23 | return lhs.layer == rhs.layer && lhs.item == rhs.item 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /KRTournamentView/Classes/Private/Defaults.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Defaults.swift 3 | // KRTournamentView 4 | // 5 | // Copyright © 2018 Krimpedance. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | struct Default { 11 | static let style = KRTournamentViewStyle.left 12 | static let tournamentStructure = TournamentBuilder.build(numberOfLayers: 2) 13 | static let lineColor = UIColor.black 14 | static let winnerLineColor = UIColor.red 15 | static let lineWidth = CGFloat(1.0) 16 | static let fixOrientation: Bool = false 17 | 18 | static var tournamentViewEntry: KRTournamentViewEntry { return KRTournamentViewEntry() } 19 | static var tournamentViewMatch: KRTournamentViewMatch { return KRTournamentViewMatch() } 20 | 21 | static func entrySize(with style: KRTournamentViewStyle) -> CGSize { 22 | return style.isVertical ? CGSize(width: 80, height: 30) : CGSize(width: 30, height: 80) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /KRTournamentView/Classes/Private/KRTournamentDrawingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KRTournamentDrawingView.swift 3 | // KRTournamentView 4 | // 5 | // Copyright © 2018 Krimpedance. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | final class KRTournamentDrawingView: UIView { 11 | private weak var dataStore: KRTournamentViewDataStore! 12 | 13 | var matches = [KRTournamentViewMatch]() { 14 | willSet { matches.forEach { $0.removeFromSuperview() } } 15 | } 16 | 17 | convenience init(dataStore: KRTournamentViewDataStore) { 18 | self.init(frame: .zero) 19 | self.dataStore = dataStore 20 | backgroundColor = .clear 21 | translatesAutoresizingMaskIntoConstraints = false 22 | } 23 | 24 | override func draw(_ rect: CGRect) { 25 | super.draw(rect) 26 | 27 | let pathSet = getPath() 28 | 29 | dataStore.lineColor.setStroke() 30 | pathSet.path.lineWidth = dataStore.lineWidth 31 | pathSet.path.lineCapStyle = .square 32 | pathSet.path.stroke() 33 | 34 | dataStore.winnerLineColor.setStroke() 35 | pathSet.winnerPath.lineWidth = dataStore.winnerLineWidth ?? dataStore.lineWidth 36 | pathSet.winnerPath.lineCapStyle = .square 37 | pathSet.winnerPath.stroke() 38 | 39 | matches.forEach { addSubview($0) } 40 | } 41 | } 42 | 43 | // MARK: - Private actions ------------ 44 | 45 | private extension KRTournamentDrawingView { 46 | func getPath() -> PathSet { 47 | let drawHalf: DrawHalf = { 48 | switch dataStore.style { 49 | case .left, .top, .leftRight, .topBottom: return .first 50 | case .right, .bottom: return .second 51 | } 52 | }() 53 | 54 | return dataStore.tournamentStructure.getPath(with: .init( 55 | structure: dataStore.tournamentStructure, 56 | style: dataStore.style, 57 | drawHalf: drawHalf, 58 | rect: bounds, 59 | entrySize: dataStore.entrySize 60 | )) { matchPath, frame, points in 61 | guard let match = matches.first(where: { $0.matchPath == matchPath }) else { return } 62 | match.frame = frame 63 | match.winnerPoints = points 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /KRTournamentView/Classes/Private/KRTournamentEntriesView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KRTournamentEntriesView.swift 3 | // KRTournamentView 4 | // 5 | // Copyright © 2018 Krimpedance. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | // MARK: - Protocols ------------ 11 | 12 | protocol EntriesViewTournamentInfoProtocol { 13 | var entrySize: CGSize { get } 14 | init(structure: Bracket, style: KRTournamentViewStyle, drawHalf: DrawHalf, rect: CGRect, entrySize: CGSize) 15 | func entryPoint(at index: Int) -> CGPoint 16 | } 17 | 18 | extension TournamentInfo: EntriesViewTournamentInfoProtocol {} 19 | 20 | // MARK: - KRTournamentEntriesView ------------ 21 | 22 | final class KRTournamentEntriesView: UIView { 23 | private weak var dataStore: KRTournamentViewDataStore! 24 | private var drawHalf: DrawHalf! 25 | private var info: EntriesViewTournamentInfoProtocol! 26 | 27 | var tournamentInfoType: EntriesViewTournamentInfoProtocol.Type = TournamentInfo.self // for test 28 | 29 | var entries = [KRTournamentViewEntry]() { 30 | willSet { entries.forEach { $0.removeFromSuperview() } } 31 | didSet { updateLayout() } 32 | } 33 | 34 | convenience init(dataStore: KRTournamentViewDataStore, drawHalf: DrawHalf) { 35 | self.init(frame: .zero) 36 | self.dataStore = dataStore 37 | self.drawHalf = drawHalf 38 | backgroundColor = .clear 39 | translatesAutoresizingMaskIntoConstraints = false 40 | } 41 | 42 | override func layoutSubviews() { 43 | super.layoutSubviews() 44 | 45 | info = tournamentInfoType.init( 46 | structure: dataStore.tournamentStructure, 47 | style: dataStore.style, 48 | drawHalf: drawHalf, 49 | rect: bounds, 50 | entrySize: dataStore.entrySize 51 | ) 52 | 53 | updateLayout() 54 | } 55 | } 56 | 57 | // MARK: - Actions ------------ 58 | 59 | private extension KRTournamentEntriesView { 60 | func updateLayout() { 61 | if info == nil { return } 62 | 63 | entries.forEach { entry in 64 | let point = info.entryPoint(at: entry.index) 65 | 66 | if dataStore.style.isVertical { 67 | let origin = CGPoint(x: 0, y: point.y - info.entrySize.height / 2) 68 | entry.frame = CGRect(origin: origin, size: info.entrySize) 69 | } else { 70 | let origin = CGPoint(x: point.x - info.entrySize.width / 2, y: 0) 71 | entry.frame = CGRect(origin: origin, size: info.entrySize) 72 | } 73 | 74 | addSubview(entry) 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /KRTournamentView/Classes/Private/KRTournamentViewDataStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KRTournamentViewDataStore.swift 3 | // KRTournamentView 4 | // 5 | // Copyright © 2018 Krimpedance. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol KRTournamentViewDataStore: class { 11 | var style: KRTournamentViewStyle { get } 12 | var tournamentStructure: Bracket { get } 13 | var entrySize: CGSize { get } 14 | var lineColor: UIColor { get } 15 | var winnerLineColor: UIColor { get } 16 | var lineWidth: CGFloat { get } 17 | var winnerLineWidth: CGFloat? { get } 18 | } 19 | -------------------------------------------------------------------------------- /KRTournamentView/Classes/Private/NSLayoutConstraint+Ex.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSLayoutConstraintExtension.swift 3 | // KRTournamentView 4 | // 5 | // Copyright © 2018 Krimpedance. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | extension NSLayoutConstraint { 11 | convenience init(item view1: Any, attribute attr1: NSLayoutConstraint.Attribute, relatedBy relation: NSLayoutConstraint.Relation = .equal, toItem view2: Any? = nil, attribute attr2: NSLayoutConstraint.Attribute? = nil, constant: CGFloat = 0) { 12 | self.init(item: view1, attribute: attr1, relatedBy: relation, toItem: view2, attribute: attr2 ?? attr1, multiplier: 1.0, constant: constant) 13 | } 14 | 15 | static func constraints(from view1: Any, to view2: Any, attributes: [NSLayoutConstraint.Attribute]) -> [NSLayoutConstraint] { 16 | return attributes.map { attr in 17 | NSLayoutConstraint(item: view1, attribute: attr, toItem: view2) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /KRTournamentView/Classes/Private/PathSet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PathSet.swift 3 | // KRTournamentView 4 | // 5 | // Copyright © 2019 Krimpedance. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | struct PathSet { 11 | let path: UIBezierPath 12 | let winnerPath: UIBezierPath 13 | 14 | subscript(needsWinnerPath: Bool) -> UIBezierPath { 15 | return needsWinnerPath ? winnerPath : path 16 | } 17 | 18 | init(path: UIBezierPath = .init(), winnerPath: UIBezierPath = .init()) { 19 | self.path = path 20 | self.winnerPath = winnerPath 21 | } 22 | 23 | func append(_ pathSet: PathSet) { 24 | path.append(pathSet.path) 25 | winnerPath.append(pathSet.winnerPath) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /KRTournamentView/Classes/Private/TournamentInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KRTournamentCalculatable.swift 3 | // KRTournamentView 4 | // 5 | // Copyright © 2019 Krimpedance. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | enum DrawHalf { case first, second } 11 | 12 | struct TournamentInfo { 13 | let numberOfLayer: Int 14 | let style: KRTournamentViewStyle 15 | let drawHalf: DrawHalf 16 | let rect: CGRect 17 | let firstEntryNum: Int 18 | let secondEntryNum: Int 19 | 20 | let entrySize: CGSize 21 | let stepSize: CGSize 22 | let drawMargin: CGFloat 23 | 24 | // Initializers ------------ 25 | 26 | init( 27 | numberOfLayer: Int, 28 | style: KRTournamentViewStyle, 29 | drawHalf: DrawHalf, 30 | rect: CGRect, 31 | firstEntryNum: Int, 32 | secondEntryNum: Int, 33 | entrySize: CGSize, 34 | stepSize: CGSize, 35 | drawMargin: CGFloat 36 | ) { 37 | self.numberOfLayer = numberOfLayer 38 | self.style = style 39 | self.drawHalf = drawHalf 40 | self.rect = rect 41 | self.firstEntryNum = firstEntryNum 42 | self.secondEntryNum = secondEntryNum 43 | self.entrySize = entrySize 44 | self.stepSize = stepSize 45 | self.drawMargin = drawMargin 46 | } 47 | 48 | init( 49 | numberOfLayer: Int, 50 | style: KRTournamentViewStyle, 51 | drawHalf: DrawHalf, 52 | rect: CGRect, 53 | firstEntryNum: Int, 54 | secondEntryNum: Int, 55 | entrySize: CGSize 56 | ) { 57 | let entryNum = (drawHalf == .first) ? firstEntryNum : secondEntryNum 58 | 59 | let entrySize: CGSize = { 60 | let length = style.isVertical ? entrySize.height : entrySize.width 61 | let frameLength = style.isVertical ? rect.height : rect.width 62 | let maxLength = frameLength / max(CGFloat(entryNum), 1) 63 | return style.isVertical ? 64 | .init(width: entrySize.width, height: min(length, maxLength)) : 65 | .init(width: min(length, maxLength), height: entrySize.height) 66 | }() 67 | 68 | let drawMargin = style.isVertical ? entrySize.height / 2 : entrySize.width / 2 69 | 70 | let stepSize: CGSize = { 71 | let layers = style.isHalf ? numberOfLayer + 1 : numberOfLayer * 2 72 | return style.isVertical 73 | ? .init( 74 | width: rect.width / CGFloat(layers), 75 | height: (rect.height - entrySize.height) / max(CGFloat(entryNum - 1), 1) 76 | ) 77 | : .init( 78 | width: (rect.width - entrySize.width) / max(CGFloat(entryNum - 1), 1), 79 | height: rect.height / CGFloat(layers) 80 | ) 81 | }() 82 | 83 | self.init( 84 | numberOfLayer: numberOfLayer, 85 | style: style, 86 | drawHalf: drawHalf, 87 | rect: rect, 88 | firstEntryNum: firstEntryNum, 89 | secondEntryNum: secondEntryNum, 90 | entrySize: entrySize, 91 | stepSize: stepSize, 92 | drawMargin: drawMargin 93 | ) 94 | } 95 | 96 | init(structure: Bracket, style: KRTournamentViewStyle, drawHalf: DrawHalf, rect: CGRect, entrySize: CGSize) { 97 | self.init( 98 | numberOfLayer: structure.matchPath.layer, 99 | style: style, 100 | drawHalf: drawHalf, 101 | rect: rect, 102 | firstEntryNum: structure.entries(style: style, drawHalf: .first).count, 103 | secondEntryNum: structure.entries(style: style, drawHalf: .second).count, 104 | entrySize: entrySize 105 | ) 106 | } 107 | } 108 | 109 | // MARK: - Actions ------------ 110 | 111 | extension TournamentInfo { 112 | func convert(drawHalf: DrawHalf) -> TournamentInfo { 113 | return .init( 114 | numberOfLayer: numberOfLayer, 115 | style: style, 116 | drawHalf: drawHalf, 117 | rect: rect, 118 | firstEntryNum: firstEntryNum, 119 | secondEntryNum: secondEntryNum, 120 | entrySize: entrySize 121 | ) 122 | } 123 | 124 | func entryPoint(at index: Int) -> CGPoint { 125 | let entryNum = (drawHalf == .first) ? firstEntryNum : secondEntryNum 126 | let offset = (style.isHalf || drawHalf == .first) ? 0 : firstEntryNum 127 | 128 | switch style { 129 | case .left, 130 | .leftRight where drawHalf == .first: 131 | return (entryNum == 1) 132 | ? .init(x: 0, y: rect.midY) 133 | : .init(x: 0, y: drawMargin + stepSize.height * CGFloat(index)) 134 | case .top, 135 | .topBottom where drawHalf == .first: 136 | return (entryNum == 1) 137 | ? .init(x: rect.midX, y: 0) 138 | : .init(x: drawMargin + stepSize.width * CGFloat(index), y: 0) 139 | case .right, .leftRight: 140 | return (entryNum == 1) 141 | ? .init(x: rect.width, y: rect.midY) 142 | : .init(x: rect.width, y: drawMargin + stepSize.height * CGFloat(index - offset)) 143 | case .bottom, .topBottom: 144 | return (entryNum == 1) 145 | ? .init(x: rect.midX, y: rect.height) 146 | : .init(x: drawMargin + stepSize.width * CGFloat(index - offset), y: rect.height) 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /KRTournamentView/Classes/TournamentStructure/Bracket.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bracket.swift 3 | // KRTournamentView 4 | // 5 | // Copyright © 2019 Krimpedance. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | /// `Bracket` represents a part of tournament like the internal node in the field of tree structure. 11 | public struct Bracket: TournamentStructure, CustomStringConvertible { 12 | /// `MatchPath` 13 | public let matchPath: MatchPath! 14 | 15 | /// Child structures. 16 | public let children: [TournamentStructure] 17 | 18 | /// Number of winners as structure. 19 | public let numberOfWinners: Int 20 | 21 | /// Indexes of winning entries. 22 | public let winnerIndexes: [Int] 23 | 24 | /// A textual representation 25 | public var description: String { return descriptions.joined(separator: "\n") } 26 | 27 | /// Initializer 28 | /// 29 | /// - Parameters: 30 | /// - children: Child structures. 31 | /// - numberOfWinners: number of winners. 32 | /// - winnerIndexes: Indexes of winning entries. 33 | public init(children: [TournamentStructure], numberOfWinners: Int = 1, winnerIndexes: [Int] = []) { 34 | precondition(numberOfWinners > 0, "numberOfWinner must be greater than 0") 35 | 36 | self.matchPath = nil 37 | self.children = children.filter { ($0 is Bracket) || ($0 is Entry) } 38 | self.numberOfWinners = numberOfWinners 39 | self.winnerIndexes = winnerIndexes 40 | } 41 | } 42 | 43 | // MARK: - Internal actions ------------ 44 | 45 | extension Bracket { 46 | init(matchPath: MatchPath, children: [TournamentStructure], numberOfWinners: Int = 1, winnerIndexes: [Int] = []) { 47 | self.matchPath = matchPath 48 | self.children = children.filter { ($0 is Bracket) || ($0 is Entry) } 49 | self.numberOfWinners = numberOfWinners 50 | self.winnerIndexes = winnerIndexes 51 | } 52 | 53 | func getMatchPaths() -> [MatchPath] { 54 | let (entryNum, childMatchPaths) = children.reduce((0, [])) { result, child -> (Int, [MatchPath]) in 55 | if child is Entry { return (result.0 + 1, result.1) } 56 | guard let bracket = child as? Bracket else { return result } 57 | return (result.0 + bracket.numberOfWinners, result.1 + bracket.getMatchPaths()) 58 | } 59 | return (entryNum == 1) ? childMatchPaths : [matchPath] + childMatchPaths 60 | } 61 | 62 | func formatted(force: Bool) -> Bracket { 63 | if !force && matchPath != nil { return self } 64 | 65 | let layer = searchNumberOfLayer() 66 | var entryIndex = 0 67 | var matchNumbers = [Int: Int]() 68 | (0...layer).forEach { matchNumbers[$0] = 0 } 69 | 70 | let children = self.children.compactMap { child -> TournamentStructure? in 71 | if var bracket = child as? Bracket { 72 | let childLayer = layer - 1 73 | defer { matchNumbers[childLayer]! += 1 } 74 | return bracket.formatted( 75 | matchPath: .init(layer: childLayer, item: matchNumbers[childLayer]!), 76 | matchNumbers: &matchNumbers, 77 | entryIndex: &entryIndex 78 | ) 79 | } 80 | if var entry = child as? Entry { 81 | defer { entryIndex += 1 } 82 | return Entry(index: entryIndex) 83 | } 84 | return nil 85 | } 86 | 87 | return Bracket(matchPath: .init(layer: layer, item: 0), children: children, numberOfWinners: numberOfWinners, winnerIndexes: winnerIndexes) 88 | } 89 | 90 | private func formatted(matchPath: MatchPath, matchNumbers: inout [Int: Int], entryIndex: inout Int) -> Bracket { 91 | let children = self.children.compactMap { child -> TournamentStructure? in 92 | if var bracket = child as? Bracket { 93 | let childLayer = matchPath.layer - 1 94 | defer { matchNumbers[childLayer]! += 1 } 95 | return bracket.formatted( 96 | matchPath: .init(layer: childLayer, item: matchNumbers[childLayer]!), 97 | matchNumbers: &matchNumbers, 98 | entryIndex: &entryIndex 99 | ) 100 | } 101 | if var entry = child as? Entry { 102 | defer { entryIndex += 1 } 103 | return Entry(index: entryIndex) 104 | } 105 | return nil 106 | } 107 | 108 | return .init(matchPath: matchPath, children: children, numberOfWinners: numberOfWinners, winnerIndexes: winnerIndexes) 109 | } 110 | } 111 | 112 | // MARK: - Public actions ------------ 113 | 114 | public extension Bracket { 115 | /// Validates values, set Bracket.matchPath and Entry.index to all structures. 116 | mutating func format() { 117 | self = formatted(force: true) 118 | } 119 | 120 | /// Validates values, and returns bracket set Bracket.matchPath and Entry.index to all structures. 121 | func formatted() -> Bracket { 122 | return formatted(force: true) 123 | } 124 | } 125 | 126 | // MARK: - Private extensions ------------ 127 | 128 | private extension TournamentStructure { 129 | var descriptions: [String] { 130 | if let entry = self as? Entry { return [entry.description] } 131 | guard let bracket = self as? Bracket else { return [] } 132 | 133 | return ["🔸[\(bracket.matchPath.layer)-\(bracket.matchPath.item)]"] + bracket.children.flatMap { child in 134 | return child.descriptions.enumerated().map { index, desc in 135 | return (index == 0) 136 | ? " ▹︎ " + desc 137 | : " " + desc 138 | } 139 | } 140 | } 141 | 142 | func searchNumberOfLayer() -> Int { 143 | guard let bracket = self as? Bracket else { return 0 } 144 | if bracket.children.count == 0 { return 1 } 145 | return bracket.children.map { $0.searchNumberOfLayer() }.max()! + 1 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /KRTournamentView/Classes/TournamentStructure/Entry.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Entry.swift 3 | // KRTournamentView 4 | // 5 | // Copyright © 2019 Krimpedance. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | /// `Entry` represents a first entry of tournament like the leaf node in the field of tree structure. 11 | public struct Entry: TournamentStructure, CustomStringConvertible { 12 | let index: Int! 13 | 14 | /// A textual representation 15 | public var description: String { return "👤_\(index!)" } 16 | 17 | // Initializers ------------ 18 | 19 | init(index: Int) { self.index = index } 20 | 21 | /// Initializer 22 | public init() { index = nil } 23 | } 24 | -------------------------------------------------------------------------------- /KRTournamentView/Classes/TournamentStructure/TournamentBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TournamentBuilder.swift 3 | // KRTournamentView 4 | // 5 | // Copyright © 2019 Krimpedance. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Builder of `Bracket` 11 | public class TournamentBuilder: Equatable { 12 | /// Builder Entry 13 | public enum BuildType: Equatable { case entry, bracket(TournamentBuilder) } 14 | 15 | /// number of winners. 16 | public var numberOfWinners = 1 17 | /// Indexes of winning entries. 18 | public var winnerIndexes = [Int]() 19 | /// children 20 | public var children = [BuildType]() 21 | 22 | /// Initializer 23 | /// 24 | /// - Parameters: 25 | /// - children: array of BuildType. 26 | /// - numberOfWinners: number of winners. 27 | /// - winnerIndexes: Indexes of winning entries. 28 | public init(children: [BuildType] = [], numberOfWinners: Int = 1, winnerIndexes: [Int] = []) { 29 | self.children = children 30 | self.numberOfWinners = numberOfWinners 31 | self.winnerIndexes = winnerIndexes 32 | } 33 | 34 | /// Initialize with symmetrical children. 35 | /// 36 | /// - Parameters: 37 | /// - numberOfLayers: number of layers. 38 | /// - numberOfEntries: number of entries for each bracket. 39 | /// - numberOfWinners: number of winners for each bracket. 40 | /// - handler: handler returns Indexes of winning entries. 41 | /// - Returns: TournamentBuilder 42 | public init(numberOfLayers: Int, numberOfEntries: Int = 2, numberOfWinners: Int = 1, handler: ((MatchPath) -> [Int])? = nil) { 43 | precondition(numberOfLayers > 0, "numberOfLayers must be greater than 0") 44 | precondition( 45 | numberOfLayers == 1 || numberOfEntries.divisors.contains(numberOfWinners), 46 | "numberOfWinners must be divisor of numberOfEntries: \(numberOfEntries) -> \(numberOfEntries.divisors)" 47 | ) 48 | 49 | func _init(layer: Int, num: Int) -> TournamentBuilder { 50 | let winnerIndexes = handler?(.init(layer: layer, item: num)) ?? [] 51 | let children: [BuildType] = { 52 | switch layer { 53 | case 1: 54 | return (0.. Bool { 75 | return (lhs.numberOfWinners == rhs.numberOfWinners) 76 | && (lhs.winnerIndexes == rhs.winnerIndexes) 77 | && (lhs.children == rhs.children) 78 | } 79 | 80 | /// Build symmetrical bracket. 81 | /// 82 | /// - Parameters: 83 | /// - numberOfLayers: number of layers. 84 | /// - numberOfEntries: number of entries for each bracket. 85 | /// - numberOfWinners: number of winners for each bracket. 86 | /// - handler: handler returns Indexes of winning entries. 87 | /// - Returns: formatted Bracket instance 88 | public static func build(numberOfLayers: Int, numberOfEntries: Int = 2, numberOfWinners: Int = 1, handler: ((MatchPath) -> [Int])? = nil) -> Bracket { 89 | return TournamentBuilder(numberOfLayers: numberOfLayers, numberOfEntries: numberOfEntries, numberOfWinners: numberOfWinners, handler: handler).build(format: true) 90 | } 91 | } 92 | 93 | // MARK: - Private actions ------------ 94 | 95 | private extension TournamentBuilder { 96 | func getChildBuilder(for matchPath: MatchPath, ownMatchPath: MatchPath, matchNumbers: inout [Int: Int]) -> TournamentBuilder? { 97 | if matchPath == ownMatchPath { return self } 98 | if matchPath.layer == ownMatchPath.layer { return nil } 99 | 100 | return children.compactMap { child -> TournamentBuilder? in 101 | guard case let .bracket(builder) = child else { return nil } 102 | let childLayer = ownMatchPath.layer - 1 103 | defer { matchNumbers[childLayer]! += 1 } 104 | let childMatchPath = MatchPath(layer: childLayer, item: matchNumbers[childLayer]!) 105 | return builder.getChildBuilder(for: matchPath, ownMatchPath: childMatchPath, matchNumbers: &matchNumbers) 106 | }.first 107 | } 108 | 109 | func searchNumberOfLayer() -> Int { 110 | if children.count == 0 { return 1 } 111 | return children.map { 112 | guard case let .bracket(builder) = $0 else { return 0 } 113 | return builder.searchNumberOfLayer() 114 | }.max()! + 1 115 | } 116 | } 117 | 118 | // MARK: - Public actions ------------ 119 | 120 | public extension TournamentBuilder { 121 | /// Sets number of winners. 122 | /// 123 | /// - Parameter num: number of winners. 124 | /// - Returns: this instance. 125 | @discardableResult 126 | func setNumberOfWinners(_ num: Int) -> TournamentBuilder { 127 | numberOfWinners = num 128 | return self 129 | } 130 | 131 | /// Sets indexes of winning entries. 132 | /// 133 | /// - Parameter indexes: indexes of winning entries. 134 | /// - Returns: this instance. 135 | @discardableResult 136 | func setWinnerIndexes(_ indexes: [Int]) -> TournamentBuilder { 137 | winnerIndexes = indexes 138 | return self 139 | } 140 | 141 | /// Add `Entry` to children. 142 | /// 143 | /// - Parameter num: number of entries to add. 144 | /// - Returns: this instance. 145 | @discardableResult 146 | func addEntry(_ num: Int = 1) -> TournamentBuilder { 147 | (0.. TournamentBuilder) -> TournamentBuilder { 157 | children.append(.bracket(handler())) 158 | return self 159 | } 160 | 161 | /// Build from currnt state. 162 | /// 163 | /// - Parameter format: If true, call `.format()` method after build `Bracket`. 164 | /// - Returns: Bracket instance. 165 | func build(format: Bool = false) -> Bracket { 166 | let structures: [TournamentStructure] = children.map { 167 | switch $0 { 168 | case .entry: return Entry() 169 | case .bracket(let builder): return builder.build() 170 | } 171 | } 172 | let bracket = Bracket(children: structures, numberOfWinners: numberOfWinners, winnerIndexes: winnerIndexes) 173 | return format ? bracket.formatted() : bracket 174 | } 175 | 176 | func getChildBuilder(for matchPath: MatchPath) -> TournamentBuilder? { 177 | let layer = searchNumberOfLayer() 178 | 179 | if layer < matchPath.layer { return nil } 180 | if layer == matchPath.layer { return (matchPath.item == 0) ? self : nil } 181 | 182 | var matchNumbers = [Int: Int]() 183 | (0...layer).forEach { matchNumbers[$0] = 0 } 184 | 185 | return children.compactMap { child -> TournamentBuilder? in 186 | guard case let .bracket(builder) = child else { return nil } 187 | let childLayer = layer - 1 188 | defer { matchNumbers[childLayer]! += 1 } 189 | let childMatchPath = MatchPath(layer: childLayer, item: matchNumbers[childLayer]!) 190 | return builder.getChildBuilder(for: matchPath, ownMatchPath: childMatchPath, matchNumbers: &matchNumbers) 191 | }.first 192 | } 193 | } 194 | 195 | // MARK: - Extensions ------------ 196 | 197 | extension Int { 198 | var divisors: [Int] { 199 | if self < 1 { return [] } 200 | return (1...Swift.max(1, self/2)).filter { self % $0 == 0 } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /KRTournamentView/Classes/TournamentStructure/TournamentStructure.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TournamentStructure.swift 3 | // KRTournamentView 4 | // 5 | // Copyright © 2019 Krimpedance. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | public protocol TournamentStructure { 11 | } 12 | 13 | extension TournamentStructure { 14 | private var entries: [Entry] { 15 | if let entry = self as? Entry { return [entry] } 16 | if let bracket = self as? Bracket { return bracket.children.flatMap { $0.entries } } 17 | return [] 18 | } 19 | 20 | func entries(style: KRTournamentViewStyle, drawHalf: DrawHalf) -> [Entry] { 21 | if let entry = self as? Entry { return [entry] } 22 | guard let bracket = self as? Bracket else { return [] } 23 | 24 | switch style { 25 | case .leftRight, .topBottom: 26 | let count = bracket.children.count 27 | return (drawHalf == .first) 28 | ? bracket.children[0.. Bool { 41 | if case let (lEntry?, rEntry?) = (lhs as? Entry, rhs as? Entry) { return lEntry.index == rEntry.index } 42 | guard case let (lBracket?, rBracket?) = (lhs as? Bracket, rhs as? Bracket) else { return false } 43 | 44 | var isEqualMatchPath: Bool { 45 | switch (lBracket.matchPath, rBracket.matchPath) { 46 | case (nil, nil): return true 47 | case let (lMP?, rMP?): return lMP == rMP 48 | default: return false 49 | } 50 | } 51 | 52 | return isEqualMatchPath 53 | && lBracket.children == rBracket.children 54 | && lBracket.numberOfWinners == rBracket.numberOfWinners 55 | && lBracket.winnerIndexes == rBracket.winnerIndexes 56 | } 57 | 58 | public func != (lhs: TournamentStructure, rhs: TournamentStructure) -> Bool { 59 | return !(lhs == rhs) 60 | } 61 | 62 | public func == (lhs: [TournamentStructure], rhs: [TournamentStructure]) -> Bool { 63 | if lhs.count != rhs.count { return false } 64 | 65 | for (index, lItem) in lhs.enumerated() where lItem != rhs[index] { 66 | return false 67 | } 68 | 69 | return true 70 | } 71 | -------------------------------------------------------------------------------- /KRTournamentView/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /KRTournamentView/KRTournamentView.h: -------------------------------------------------------------------------------- 1 | // 2 | // KRTournamentView.h 3 | // KRTournamentView 4 | // 5 | // Copyright © 2018 Krimpedance. All rights reserved. 6 | // 7 | 8 | @import Foundation; 9 | 10 | //! Project version number for KRTournamentView. 11 | FOUNDATION_EXPORT double KRTournamentViewVersionNumber; 12 | 13 | //! Project version string for KRTournamentView. 14 | FOUNDATION_EXPORT const unsigned char KRTournamentViewVersionString[]; 15 | 16 | // In this header, you should import all the public headers of your framework using statements like #import 17 | 18 | 19 | -------------------------------------------------------------------------------- /KRTournamentViewTests/Fakes/KRTournamentViewEntryFake.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KRTournamentViewEntryFake.swift 3 | // KRTournamentView 4 | // 5 | // Copyright © 2019 Krimpedance. All rights reserved. 6 | // 7 | // swiftlint:disable superfluous_disable_command 8 | // swiftlint:disable identifier_name 9 | // swiftlint:disable force_try 10 | // swiftlint:disable function_body_length 11 | 12 | @testable import KRTournamentView 13 | 14 | extension KRTournamentViewEntry { 15 | static func fake(index: Int = 0) -> KRTournamentViewEntry { 16 | let entry = KRTournamentViewEntry() 17 | entry.index = index 18 | return entry 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /KRTournamentViewTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /KRTournamentViewTests/Mocks/KRTournamentViewDataSourceMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KRTournamentViewDataSourceMock.swift 3 | // KRTournamentView 4 | // 5 | // Copyright © 2019 Krimpedance. All rights reserved. 6 | // 7 | 8 | import XCTest 9 | import Quick 10 | import Nimble 11 | @testable import KRTournamentView 12 | 13 | final class KRTournamentViewDataSourceMock: KRTournamentViewDataSource { 14 | var structure: Bracket 15 | 16 | init(structure: Bracket = Default.tournamentStructure) { 17 | self.structure = structure 18 | } 19 | 20 | func structure(of tournamentView: KRTournamentView) -> Bracket { 21 | return structure 22 | } 23 | 24 | func tournamentView(_ tournamentView: KRTournamentView, entryAt index: Int) -> KRTournamentViewEntry { 25 | return Default.tournamentViewEntry 26 | } 27 | 28 | func tournamentView(_ tournamentView: KRTournamentView, matchAt matchPath: MatchPath) -> KRTournamentViewMatch { 29 | return Default.tournamentViewMatch 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /KRTournamentViewTests/Mocks/KRTournamentViewDataStoreMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KRTournamentViewDataStoreMock.swift 3 | // KRTournamentView 4 | // 5 | // Copyright © 2019 Krimpedance. All rights reserved. 6 | // 7 | 8 | import XCTest 9 | import Quick 10 | import Nimble 11 | @testable import KRTournamentView 12 | 13 | final class KRTournamentViewDataStoreMock: KRTournamentViewDataStore { 14 | var style: KRTournamentViewStyle 15 | var tournamentStructure: Bracket 16 | var entrySize: CGSize 17 | var lineColor: UIColor 18 | var winnerLineColor: UIColor 19 | var lineWidth: CGFloat 20 | var winnerLineWidth: CGFloat? 21 | 22 | init( 23 | style: KRTournamentViewStyle = .left, 24 | tournamentStructure: Bracket = TournamentBuilder.build(numberOfLayers: 2), 25 | entrySize: CGSize = .init(width: 80, height: 30), 26 | lineColor: UIColor = .black, 27 | winnerLineColor: UIColor = .red, 28 | lineWidth: CGFloat = 2, 29 | winnerLineWidth: CGFloat? = nil 30 | ) { 31 | self.style = style 32 | self.tournamentStructure = tournamentStructure 33 | self.entrySize = entrySize 34 | self.lineColor = lineColor 35 | self.winnerLineColor = winnerLineColor 36 | self.lineWidth = lineWidth 37 | self.winnerLineWidth = winnerLineWidth 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /KRTournamentViewTests/Tests/BracketTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BracketTests.swift 3 | // KRTournamentView 4 | // 5 | // Copyright © 2019 Krimpedance. All rights reserved. 6 | // 7 | // swiftlint:disable superfluous_disable_command 8 | // swiftlint:disable identifier_name 9 | // swiftlint:disable force_try 10 | // swiftlint:disable force_cast 11 | // swiftlint:disable function_body_length 12 | 13 | import XCTest 14 | import Quick 15 | import Nimble 16 | @testable import KRTournamentView 17 | 18 | class BracketTests: QuickSpec { 19 | private struct FakeBracket: TournamentStructure {} 20 | 21 | override func spec() { 22 | // Initializer ------------ 23 | 24 | it("can initialize") { 25 | let bracket = Bracket( 26 | matchPath: .init(layer: 2, item: 1), 27 | children: [Entry(index: 2), Entry(index: 3), Entry(index: 5), FakeBracket()], 28 | numberOfWinners: 2, 29 | winnerIndexes: [0, 2] 30 | ) 31 | expect(bracket.matchPath).to(equal(.init(layer: 2, item: 1))) 32 | expect(bracket.children == [Entry(index: 2), Entry(index: 3), Entry(index: 5)]).to(beTrue()) 33 | expect(bracket.numberOfWinners).to(be(2)) 34 | expect(bracket.winnerIndexes).to(equal([0, 2])) 35 | 36 | let bracket2 = Bracket( 37 | children: [Entry(index: 2), Entry(index: 3), Entry(index: 5), FakeBracket()], 38 | numberOfWinners: 3, 39 | winnerIndexes: [0, 1, 3] 40 | ) 41 | expect(bracket2.matchPath).to(beNil()) 42 | expect(bracket2.children == [Entry(index: 2), Entry(index: 3), Entry(index: 5)]).to(beTrue()) 43 | expect(bracket2.numberOfWinners).to(be(3)) 44 | expect(bracket2.winnerIndexes).to(equal([0, 1, 3])) 45 | } 46 | 47 | it("can initialize with default value") { 48 | let bracket = Bracket( 49 | matchPath: .init(layer: 2, item: 1), 50 | children: [Entry(index: 2), Entry(index: 3), Entry(index: 5), FakeBracket()] 51 | ) 52 | expect(bracket.matchPath).to(equal(.init(layer: 2, item: 1))) 53 | expect(bracket.children == [Entry(index: 2), Entry(index: 3), Entry(index: 5)]).to(beTrue()) 54 | expect(bracket.numberOfWinners).to(be(1)) 55 | expect(bracket.winnerIndexes).to(equal([])) 56 | 57 | let bracket2 = Bracket(children: [Entry(index: 2), Entry(index: 3), Entry(index: 5), FakeBracket()]) 58 | expect(bracket2.matchPath).to(beNil()) 59 | expect(bracket2.children == [Entry(index: 2), Entry(index: 3), Entry(index: 5)]).to(beTrue()) 60 | expect(bracket2.numberOfWinners).to(be(1)) 61 | expect(bracket2.winnerIndexes).to(equal([])) 62 | } 63 | 64 | // MatchPaths ------------ 65 | 66 | it("returns matchPaths") { 67 | let brackets: [Bracket] = [ 68 | TournamentBuilder.build(numberOfLayers: 2), 69 | TournamentBuilder() 70 | .addBracket { TournamentBuilder(numberOfLayers: 2) } 71 | .addBracket { TournamentBuilder(numberOfLayers: 3) } 72 | .build(format: true), 73 | TournamentBuilder() 74 | .addBracket { TournamentBuilder(numberOfLayers: 1) } 75 | .build(format: true), 76 | TournamentBuilder() 77 | .addBracket { TournamentBuilder(numberOfLayers: 1, numberOfEntries: 4, numberOfWinners: 2) } 78 | .build(format: true) 79 | ] 80 | let matchPaths: [[MatchPath]] = [ 81 | [ 82 | .init(layer: 2, item: 0), 83 | .init(layer: 1, item: 0), .init(layer: 1, item: 1) 84 | ], 85 | [ 86 | .init(layer: 4, item: 0), 87 | .init(layer: 3, item: 0), 88 | .init(layer: 2, item: 0), .init(layer: 2, item: 1), 89 | .init(layer: 3, item: 1), 90 | .init(layer: 2, item: 2), 91 | .init(layer: 1, item: 0), .init(layer: 1, item: 1), 92 | .init(layer: 2, item: 3), 93 | .init(layer: 1, item: 2), .init(layer: 1, item: 3) 94 | ], 95 | [ 96 | .init(layer: 1, item: 0) 97 | ], 98 | [ 99 | .init(layer: 2, item: 0), 100 | .init(layer: 1, item: 0) 101 | ] 102 | ] 103 | 104 | expect(brackets.map { $0.getMatchPaths() }).to(equal(matchPaths)) 105 | } 106 | 107 | // Formatting ------------ 108 | 109 | it("returns formatted Bracket") { 110 | let brackets: [Bracket] = [ 111 | Bracket(children: [Entry(), Entry()]), 112 | Bracket(matchPath: .init(layer: 5, item: 0), children: [Entry(), Entry()]), 113 | TournamentBuilder() 114 | .addBracket { .init(children: [.entry, .entry]) } 115 | .addBracket { .init(children: [.entry, .entry, .entry]) } 116 | .build(), 117 | TournamentBuilder() 118 | .addBracket { 119 | TournamentBuilder() 120 | .addBracket { .init(children: [.entry, .entry]) } 121 | .addBracket { .init(children: [.entry, .entry]) } 122 | } 123 | .addBracket { 124 | TournamentBuilder() 125 | .addBracket { 126 | TournamentBuilder() 127 | .addBracket { .init(children: [.entry, .entry]) } 128 | .addBracket { .init(children: [.entry, .entry]) } 129 | } 130 | .addBracket { 131 | TournamentBuilder() 132 | .addBracket { .init(children: [.entry, .entry]) } 133 | .addBracket { .init(children: [.entry, .entry]) } 134 | } 135 | } 136 | .build(), 137 | TournamentBuilder() 138 | .addBracket { .init(children: [.entry, .entry]) } 139 | .build() 140 | ] 141 | 142 | let expectedBrackets: [Bracket] = [ 143 | Bracket(matchPath: .init(layer: 1, item: 0), children: [Entry(index: 0), Entry(index: 1)]), 144 | Bracket(matchPath: .init(layer: 1, item: 0), children: [Entry(index: 0), Entry(index: 1)]), 145 | Bracket(matchPath: .init(layer: 2, item: 0), children: [ 146 | Bracket(matchPath: .init(layer: 1, item: 0), children: [Entry(index: 0), Entry(index: 1)]), 147 | Bracket(matchPath: .init(layer: 1, item: 1), children: [Entry(index: 2), Entry(index: 3), Entry(index: 4)]) 148 | ]), 149 | Bracket(matchPath: .init(layer: 4, item: 0), children: [ 150 | Bracket(matchPath: .init(layer: 3, item: 0), children: [ 151 | Bracket(matchPath: .init(layer: 2, item: 0), children: [Entry(index: 0), Entry(index: 1)]), 152 | Bracket(matchPath: .init(layer: 2, item: 1), children: [Entry(index: 2), Entry(index: 3)]) 153 | ]), 154 | Bracket(matchPath: .init(layer: 3, item: 1), children: [ 155 | Bracket(matchPath: .init(layer: 2, item: 2), children: [ 156 | Bracket(matchPath: .init(layer: 1, item: 0), children: [Entry(index: 4), Entry(index: 5)]), 157 | Bracket(matchPath: .init(layer: 1, item: 1), children: [Entry(index: 6), Entry(index: 7)]) 158 | ]), 159 | Bracket(matchPath: .init(layer: 2, item: 3), children: [ 160 | Bracket(matchPath: .init(layer: 1, item: 2), children: [Entry(index: 8), Entry(index: 9)]), 161 | Bracket(matchPath: .init(layer: 1, item: 3), children: [Entry(index: 10), Entry(index: 11)]) 162 | ]) 163 | ]) 164 | ]), 165 | Bracket(matchPath: .init(layer: 2, item: 0), children: [ 166 | Bracket(matchPath: .init(layer: 1, item: 0), children: [Entry(index: 0), Entry(index: 1)]) 167 | ]) 168 | ] 169 | 170 | let formattedBrackets = brackets.map { $0.formatted(force: true) } 171 | 172 | // For check 173 | // (0.. CGPoint { 87 | if style.isVertical { 88 | let offset = entrySize.height / 2 89 | return .init(x: 0, y: entrySize.height * CGFloat(index) + offset) 90 | } else { 91 | let offset = entrySize.width / 2 92 | return .init(x: entrySize.width * CGFloat(index) + offset, y: 0) 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /KRTournamentViewTests/Tests/KRTournamentViewEntryLabelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KRTournamentViewEntryLabelTests.swift 3 | // KRTournamentView 4 | // 5 | // Copyright © 2019 Krimpedance. All rights reserved. 6 | // 7 | 8 | import XCTest 9 | import Quick 10 | import Nimble 11 | @testable import KRTournamentView 12 | 13 | class KRTournamentViewEntryLabelTests: QuickSpec { 14 | override func spec() { 15 | var label: KRTournamentViewEntryLabel! 16 | 17 | beforeEach { 18 | label = KRTournamentViewEntryLabel() 19 | label.text = "hoge" 20 | } 21 | 22 | it("text can set vertical text by verticalText") { 23 | label.verticalText = "fuga" 24 | expect(label.text).to(equal("f\nu\ng\na\n")) 25 | } 26 | 27 | it("text is not updated when verticalText is set to nil") { 28 | label.verticalText = nil 29 | expect(label.text).to(equal("hoge")) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /KRTournamentViewTests/Tests/KRTournamentViewEntryTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KRTournamentViewEntryTests.swift 3 | // KRTournamentViewTests 4 | // 5 | // Copyright © 2019 Krimpedance. All rights reserved. 6 | // 7 | 8 | import XCTest 9 | import Quick 10 | import Nimble 11 | @testable import KRTournamentView 12 | 13 | class KRTournamentViewEntryTests: QuickSpec { 14 | override func spec() { 15 | var entry: KRTournamentViewEntry! 16 | 17 | beforeEach { 18 | entry = KRTournamentViewEntry() 19 | } 20 | 21 | it("size is the default entry size") { 22 | let size = Default.entrySize(with: .left) 23 | expect(entry.frame.size).to(equal(size)) 24 | } 25 | 26 | it("backgroundColor is .clear") { 27 | expect(entry.backgroundColor).to(be(UIColor.clear)) 28 | } 29 | 30 | it("clipsToBounds is true") { 31 | expect(entry.clipsToBounds).to(be(true)) 32 | } 33 | 34 | describe("textLabel") { self.textLabelSpec() } 35 | } 36 | 37 | func textLabelSpec() { 38 | var entry: KRTournamentViewEntry! 39 | var textLabel: KRTournamentViewEntryLabel! 40 | 41 | beforeEach { 42 | entry = KRTournamentViewEntry() 43 | textLabel = entry.textLabel 44 | entry.layoutIfNeeded() 45 | } 46 | 47 | it("superview is the entry") { 48 | expect(textLabel.superview).to(be(entry)) 49 | } 50 | 51 | it("backgroundColor is .clear") { 52 | expect(textLabel.backgroundColor).to(be(UIColor.clear)) 53 | } 54 | 55 | it("textAlignment is .center") { 56 | expect(textLabel.textAlignment).to(equal(NSTextAlignment.center)) 57 | } 58 | 59 | it("numberOfLines is 0") { 60 | expect(textLabel.numberOfLines).to(be(0)) 61 | } 62 | 63 | it("frame is same as entry bounds") { 64 | expect(textLabel.frame).to(equal(entry.bounds)) 65 | } 66 | 67 | it("frame is same as entry bounds when entry size is changed") { 68 | let size = Default.entrySize(with: .left) 69 | entry.frame.size = CGSize(width: size.width + 10, height: size.height + 100) 70 | entry.layoutIfNeeded() 71 | expect(textLabel.frame).to(equal(entry.bounds)) 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /KRTournamentViewTests/Tests/KRTournamentViewStyleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KRTournamentViewStyleTests.swift 3 | // KRTournamentViewTests 4 | // 5 | // Copyright © 2018 Krimpedance. All rights reserved. 6 | // 7 | // swiftlint:disable function_body_length 8 | 9 | import XCTest 10 | import Quick 11 | import Nimble 12 | @testable import KRTournamentView 13 | 14 | class KRTournamentViewStyleTests: QuickSpec { 15 | override func spec() { 16 | it("has both side styles") { 17 | expect(KRTournamentViewStyle.leftRight).to(equal(.leftRight(direction: .top))) 18 | expect(KRTournamentViewStyle.topBottom).to(equal(.topBottom(direction: .right))) 19 | } 20 | 21 | it("has direction") { 22 | let styles = KRTournamentViewStyle.getList() 23 | let directions: [KRTournamentViewStyle.FinalDirection] = [ 24 | .right, .left, .bottom, .top, 25 | .top, .top, .top, .bottom, 26 | .left, .right, .right, .right 27 | ] 28 | 29 | expect(styles.map { $0.direction }).to(equal(directions)) 30 | } 31 | 32 | it("has isVertical") { 33 | let styles = KRTournamentViewStyle.getList() 34 | let values: [Bool] = [ 35 | true, true, false, false, 36 | true, true, true, true, 37 | false, false, false, false 38 | ] 39 | 40 | expect(styles.map { $0.isVertical }).to(equal(values)) 41 | } 42 | 43 | it("has isHalf") { 44 | let styles = KRTournamentViewStyle.getList() 45 | let values: [Bool] = [ 46 | true, true, true, true, 47 | false, false, false, false, 48 | false, false, false, false 49 | ] 50 | 51 | expect(styles.map { $0.isHalf }).to(equal(values)) 52 | } 53 | 54 | // Rotation ------------ 55 | 56 | it("can switch to style rotate left") { 57 | func rotate(_ style: KRTournamentViewStyle) -> KRTournamentViewStyle { 58 | var style = style 59 | style.rotateLeft() 60 | return style 61 | } 62 | 63 | expect(rotate(.left)).to(equal(.bottom)) 64 | expect(rotate(.right)).to(equal(.top)) 65 | expect(rotate(.top)).to(equal(.left)) 66 | expect(rotate(.bottom)).to(equal(.right)) 67 | expect(rotate(.leftRight(direction: .top))).to(equal(.topBottom(direction: .left))) 68 | expect(rotate(.leftRight(direction: .bottom))).to(equal(.topBottom(direction: .right))) 69 | expect(rotate(.topBottom(direction: .right))).to(equal(.leftRight(direction: .top))) 70 | expect(rotate(.topBottom(direction: .left))).to(equal(.leftRight(direction: .bottom))) 71 | } 72 | 73 | it("can switch to style rotate right") { 74 | func rotate(_ style: KRTournamentViewStyle) -> KRTournamentViewStyle { 75 | var style = style 76 | style.rotateRight() 77 | return style 78 | } 79 | 80 | expect(rotate(.left)).to(equal(.top)) 81 | expect(rotate(.right)).to(equal(.bottom)) 82 | expect(rotate(.top)).to(equal(.right)) 83 | expect(rotate(.bottom)).to(equal(.left)) 84 | expect(rotate(.leftRight(direction: .top))).to(equal(.topBottom(direction: .right))) 85 | expect(rotate(.leftRight(direction: .bottom))).to(equal(.topBottom(direction: .left))) 86 | expect(rotate(.topBottom(direction: .right))).to(equal(.leftRight(direction: .bottom))) 87 | expect(rotate(.topBottom(direction: .left))).to(equal(.leftRight(direction: .top))) 88 | } 89 | 90 | it("can switch to reversed style") { 91 | func reverse(_ style: KRTournamentViewStyle) -> KRTournamentViewStyle { 92 | var style = style 93 | style.reverse() 94 | return style 95 | } 96 | 97 | expect(reverse(.left)).to(equal(.right)) 98 | expect(reverse(.right)).to(equal(.left)) 99 | expect(reverse(.top)).to(equal(.bottom)) 100 | expect(reverse(.bottom)).to(equal(.top)) 101 | expect(reverse(.leftRight(direction: .top))).to(equal(.leftRight(direction: .bottom))) 102 | expect(reverse(.leftRight(direction: .bottom))).to(equal(.leftRight(direction: .top))) 103 | expect(reverse(.topBottom(direction: .right))).to(equal(.topBottom(direction: .left))) 104 | expect(reverse(.topBottom(direction: .left))).to(equal(.topBottom(direction: .right))) 105 | } 106 | } 107 | } 108 | 109 | // MARK: - KRTournamentViewStyle extensions ------------ 110 | 111 | extension KRTournamentViewStyle { 112 | static func getList() -> [KRTournamentViewStyle] { 113 | let directions: [FinalDirection] = [.left, .right, .top, .bottom] 114 | let allStyles: [KRTournamentViewStyle] = [.left, .right, .top, .bottom] 115 | + directions.map { .leftRight(direction: $0) } 116 | + directions.map { .topBottom(direction: $0) } 117 | return allStyles 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /KRTournamentViewTests/Tests/KRTournamentViewTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KRTournamentViewTests.swift 3 | // KRTournamentViewTests 4 | // 5 | // Copyright © 2018 Krimpedance. All rights reserved. 6 | // 7 | 8 | import XCTest 9 | import Quick 10 | import Nimble 11 | @testable import KRTournamentView 12 | 13 | class KRTournamentViewTests: QuickSpec { 14 | 15 | override func spec() { 16 | var view: KRTournamentView! 17 | 18 | beforeEach { 19 | view = KRTournamentView(frame: CGRect(x: 0, y: 0, width: 500, height: 500)) 20 | view.layoutIfNeeded() 21 | } 22 | 23 | it("has properties") { 24 | expect(view.style).to(equal(Default.style)) 25 | expect(view.tournamentStructure == Default.tournamentStructure).to(beTrue()) 26 | expect(view.entrySize).to(equal(Default.entrySize(with: Default.style))) 27 | expect(view.lineColor).to(equal(Default.lineColor)) 28 | expect(view.winnerLineColor).to(equal(Default.winnerLineColor)) 29 | expect(view.lineWidth).to(equal(Default.lineWidth)) 30 | expect(view.winnerLineWidth).to(beNil()) 31 | expect(view.fixOrientation).to(equal(Default.fixOrientation)) 32 | } 33 | 34 | // Layouts ------------ 35 | 36 | KRTournamentViewStyle.testStyles.forEach { style in 37 | context("style is \(style)") { 38 | beforeEach { 39 | view.style = style 40 | } 41 | 42 | it("drawingView is resized") { 43 | let drawingView = view.subviews.first { $0 is KRTournamentDrawingView }! 44 | drawingView.layoutIfNeeded() 45 | view.layoutIfNeeded() 46 | expect(drawingView.frame).to(equal(style.drawingViewFrame)) 47 | } 48 | 49 | it("firstEntriesView is resized") { 50 | let entriesView = view.subviews.first { $0 is KRTournamentEntriesView }! 51 | entriesView.layoutIfNeeded() 52 | view.layoutIfNeeded() 53 | expect(entriesView.frame).to(equal(style.firstEntriesViewFrame)) 54 | } 55 | 56 | it("secondEntriesView is resized") { 57 | let entriesView = view.subviews.last { $0 is KRTournamentEntriesView }! 58 | entriesView.layoutIfNeeded() 59 | view.layoutIfNeeded() 60 | expect(entriesView.frame).to(equal(style.secondEntriesViewFrame)) 61 | } 62 | } 63 | } 64 | } 65 | } 66 | 67 | // MARK: - KRTournamentStyle extension ------------------- 68 | 69 | private extension KRTournamentViewStyle { 70 | static let testStyles = [KRTournamentViewStyle]([.left, .right, .top, .bottom, .leftRight(direction: .top), .topBottom(direction: .right)]) 71 | 72 | var drawingViewFrame: CGRect { 73 | let entrySize = Default.entrySize(with: self) 74 | switch self { 75 | case .left: 76 | return CGRect(x: entrySize.width + 5, y: 0, width: 500 - entrySize.width - 10, height: 500) 77 | case .right: 78 | return CGRect(x: 5, y: 0, width: 500 - entrySize.width - 10, height: 500) 79 | case .top: 80 | return CGRect(x: 0, y: entrySize.height + 5, width: 500, height: 500 - entrySize.height - 10) 81 | case .bottom: 82 | return CGRect(x: 0, y: 5, width: 500, height: 500 - entrySize.height - 10) 83 | case .leftRight: 84 | return CGRect(x: entrySize.width + 5, y: 0, width: 500 - (entrySize.width * 2) - 10, height: 500) 85 | case .topBottom: 86 | return CGRect(x: 0, y: entrySize.height + 5, width: 500, height: 500 - (entrySize.height * 2) - 10) 87 | } 88 | } 89 | 90 | var firstEntriesViewFrame: CGRect { 91 | let entrySize = Default.entrySize(with: self) 92 | switch self { 93 | case .left: 94 | return CGRect(x: 0, y: 0, width: entrySize.width, height: 500) 95 | case .right: 96 | return CGRect(x: 0, y: 0, width: 0, height: 500) 97 | case .top: 98 | return CGRect(x: 0, y: 0, width: 500, height: entrySize.height) 99 | case .bottom: 100 | return CGRect(x: 0, y: 0, width: 500, height: 0) 101 | case .leftRight: 102 | return CGRect(x: 0, y: 0, width: entrySize.width, height: 500) 103 | case .topBottom: 104 | return CGRect(x: 0, y: 0, width: 500, height: entrySize.height) 105 | } 106 | } 107 | 108 | var secondEntriesViewFrame: CGRect { 109 | let entrySize = Default.entrySize(with: self) 110 | switch self { 111 | case .left: 112 | return CGRect(x: 500, y: 0, width: 0, height: 500) 113 | case .right: 114 | return CGRect(x: 500 - entrySize.width, y: 0, width: entrySize.width, height: 500) 115 | case .top: 116 | return CGRect(x: 0, y: 500, width: 500, height: 0) 117 | case .bottom: 118 | return CGRect(x: 0, y: 500 - entrySize.height, width: 500, height: entrySize.height) 119 | case .leftRight: 120 | return CGRect(x: 500 - entrySize.width, y: 0, width: entrySize.width, height: 500) 121 | case .topBottom: 122 | return CGRect(x: 0, y: 500 - entrySize.height, width: 500, height: entrySize.height) 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /KRTournamentViewTests/Tests/MatchPathTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MatchPathTests.swift 3 | // KRTournamentView 4 | // 5 | // Copyright © 2019 Krimpedance. All rights reserved. 6 | // 7 | 8 | import XCTest 9 | import Quick 10 | import Nimble 11 | @testable import KRTournamentView 12 | 13 | class MatchPathTests: QuickSpec { 14 | override func spec() { 15 | it("can be initialized") { 16 | let layer = 2 17 | let item = 3 18 | let path = MatchPath(layer: layer, item: item) 19 | expect(path.layer).to(be(layer)) 20 | expect(path.item).to(be(item)) 21 | } 22 | 23 | it("is equatable") { 24 | let match = MatchPath(layer: 2, item: 0) 25 | let match2 = MatchPath(layer: 2, item: 0) 26 | let match3 = MatchPath(layer: 2, item: 1) 27 | let match4 = MatchPath(layer: 3, item: 0) 28 | 29 | expect(match).to(equal(match2)) 30 | expect(match).notTo(equal(match3)) 31 | expect(match).notTo(equal(match4)) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /KRTournamentViewTests/Tests/PathSetTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PathSetTests.swift 3 | // KRTournamentView 4 | // 5 | // Copyright © 2019 Krimpedance. All rights reserved. 6 | // 7 | 8 | import XCTest 9 | import Quick 10 | import Nimble 11 | @testable import KRTournamentView 12 | 13 | class PathSetTests: QuickSpec { 14 | override func spec() { 15 | let rect1 = CGRect(x: 0, y: 0, width: 50, height: 50) 16 | let rect2 = CGRect(x: 50, y: 50, width: 50, height: 50) 17 | 18 | var path1: UIBezierPath! 19 | var path2: UIBezierPath! 20 | var pathSet: PathSet! 21 | 22 | beforeEach { 23 | path1 = UIBezierPath(rect: rect1) 24 | path2 = UIBezierPath(rect: rect2) 25 | pathSet = PathSet(path: path1, winnerPath: path2) 26 | } 27 | 28 | it("can be initialized") { 29 | expect(pathSet.path).to(be(path1)) 30 | expect(pathSet.winnerPath).to(be(path2)) 31 | } 32 | 33 | it("has subscript") { 34 | expect(pathSet[false]).to(be(path1)) 35 | expect(pathSet[true]).to(be(path2)) 36 | } 37 | 38 | it("can append other PathSet instance") { 39 | let path3 = UIBezierPath(rect: .init(x: 50, y: 0, width: 50, height: 50)) 40 | let path4 = UIBezierPath(rect: .init(x: 0, y: 50, width: 50, height: 50)) 41 | let pathSet2 = PathSet(path: path3, winnerPath: path4) 42 | 43 | pathSet.append(pathSet2) 44 | 45 | let joinedPath1 = UIBezierPath(rect: rect1) 46 | joinedPath1.append(path3) 47 | 48 | let joinedPath2 = UIBezierPath(rect: rect2) 49 | joinedPath2.append(path4) 50 | 51 | expect(pathSet.path).to(equal(joinedPath1)) 52 | expect(pathSet.winnerPath).to(equal(joinedPath2)) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /KRTournamentViewTests/Tests/TournamentBuilderTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TournamentBuilderTests.swift 3 | // KRTournamentView 4 | // 5 | // Copyright © 2019 Krimpedance. All rights reserved. 6 | // 7 | // swiftlint:disable superfluous_disable_command 8 | // swiftlint:disable identifier_name 9 | // swiftlint:disable force_try 10 | // swiftlint:disable force_cast 11 | // swiftlint:disable function_body_length 12 | 13 | import XCTest 14 | import Quick 15 | import Nimble 16 | @testable import KRTournamentView 17 | 18 | class TournamentBuilderTests: QuickSpec { 19 | override func spec() { 20 | it("can initialize") { 21 | let builder = TournamentBuilder(children: [.entry], numberOfWinners: 2, winnerIndexes: [0, 2]) 22 | expect(builder.children).to(equal([.entry])) 23 | expect(builder.numberOfWinners).to(be(2)) 24 | expect(builder.winnerIndexes).to(equal([0, 2])) 25 | 26 | let builder2 = TournamentBuilder() 27 | expect(builder2.children.isEmpty).to(beTrue()) 28 | expect(builder2.numberOfWinners).to(be(1)) 29 | expect(builder2.winnerIndexes).to(equal([])) 30 | } 31 | 32 | it("can initialize with symmetrical children") { 33 | let builders: [TournamentBuilder] = [ 34 | .init(numberOfLayers: 1), 35 | .init(numberOfLayers: 2), 36 | .init(numberOfLayers: 1, numberOfEntries: 4, numberOfWinners: 2), 37 | .init(numberOfLayers: 2, numberOfEntries: 4, numberOfWinners: 2), 38 | .init(numberOfLayers: 2) { ($0.layer == 1) ? [0] : [1] } 39 | ] 40 | let manualBuilders: [TournamentBuilder] = [ 41 | .init(children: [.entry, .entry]), 42 | TournamentBuilder() 43 | .addBracket { .init(children: [.entry, .entry]) } 44 | .addBracket { .init(children: [.entry, .entry]) }, 45 | TournamentBuilder(numberOfWinners: 2).addEntry(4), 46 | TournamentBuilder(numberOfWinners: 2) 47 | .addBracket { TournamentBuilder(numberOfWinners: 2).addEntry(4) } 48 | .addBracket { TournamentBuilder(numberOfWinners: 2).addEntry(4) }, 49 | TournamentBuilder(winnerIndexes: [1]) 50 | .addBracket { .init(children: [.entry, .entry], winnerIndexes: [0]) } 51 | .addBracket { .init(children: [.entry, .entry], winnerIndexes: [0]) } 52 | ] 53 | 54 | expect(builders).to(equal(manualBuilders)) 55 | } 56 | 57 | // Static actions ------------ 58 | 59 | it("returns symmetrical Bracket") { 60 | let brackets: [Bracket] = [ 61 | TournamentBuilder.build(numberOfLayers: 1), 62 | TournamentBuilder.build(numberOfLayers: 2), 63 | TournamentBuilder.build(numberOfLayers: 1, numberOfEntries: 4, numberOfWinners: 2), 64 | TournamentBuilder.build(numberOfLayers: 2, numberOfEntries: 4, numberOfWinners: 2), 65 | TournamentBuilder.build(numberOfLayers: 2) { ($0.layer == 1) ? [0] : [1] } 66 | ] 67 | let expectedBrackets: [Bracket] = [ 68 | TournamentBuilder(numberOfLayers: 1).build(format: true), 69 | TournamentBuilder(numberOfLayers: 2).build(format: true), 70 | TournamentBuilder(numberOfLayers: 1, numberOfEntries: 4, numberOfWinners: 2).build(format: true), 71 | TournamentBuilder(numberOfLayers: 2, numberOfEntries: 4, numberOfWinners: 2).build(format: true), 72 | TournamentBuilder(numberOfLayers: 2) { ($0.layer == 1) ? [0] : [1] }.build(format: true) 73 | ] 74 | 75 | expect(brackets == expectedBrackets).to(beTrue()) 76 | } 77 | 78 | // Actions ------------ 79 | 80 | it("can set numberOfWinners") { 81 | let builder = TournamentBuilder() 82 | builder.setNumberOfWinners(2) 83 | expect(builder.numberOfWinners).to(be(2)) 84 | } 85 | 86 | it("can set winnerIdexes") { 87 | let builder = TournamentBuilder() 88 | builder.setWinnerIndexes([0, 1, 2]) 89 | expect(builder.winnerIndexes).to(equal([0, 1, 2])) 90 | } 91 | 92 | it("can add entries") { 93 | let builder = TournamentBuilder() 94 | builder.addEntry() 95 | expect(builder.children).to(equal([.entry])) 96 | builder.addEntry(2) 97 | expect(builder.children).to(equal([.entry, .entry, .entry])) 98 | } 99 | 100 | it("can add bracket") { 101 | let bracketBuilder = TournamentBuilder(children: [.entry, .entry], numberOfWinners: 2, winnerIndexes: [0, 1]) 102 | let builder = TournamentBuilder().addBracket { bracketBuilder } 103 | expect(builder.children).to(equal([.bracket(bracketBuilder)])) 104 | } 105 | 106 | it("keeps children order") { 107 | let builder = TournamentBuilder() 108 | builder.addEntry() 109 | builder.addBracket { .init(children: [.entry, .entry]) } 110 | builder.addEntry() 111 | 112 | let children: [TournamentBuilder.BuildType] = [.entry, .bracket(.init(children: [.entry, .entry])), .entry] 113 | expect(builder.children).to(equal(children)) 114 | } 115 | 116 | it("can build") { 117 | let bracket = TournamentBuilder() 118 | .addEntry() 119 | .addBracket { .init(children: [.entry, .entry, .entry], numberOfWinners: 2, winnerIndexes: [0, 1]) } 120 | .build() 121 | 122 | let expectedBracket = Bracket( 123 | children: [ 124 | Entry(), 125 | Bracket(children: [Entry(), Entry(), Entry()], numberOfWinners: 2, winnerIndexes: [0, 1]) 126 | ] 127 | ) 128 | 129 | expect(bracket == expectedBracket).to(beTrue()) 130 | } 131 | 132 | it("can build and format") { 133 | let bracket = TournamentBuilder() 134 | .addEntry() 135 | .addBracket { .init(children: [.entry, .entry, .entry], numberOfWinners: 2, winnerIndexes: [0, 1]) } 136 | .build(format: true) 137 | 138 | let expectedBracket = Bracket( 139 | matchPath: .init(layer: 2, item: 0), 140 | children: [ 141 | Entry(index: 0), 142 | Bracket( 143 | matchPath: .init(layer: 1, item: 0), 144 | children: [Entry(index: 1), Entry(index: 2), Entry(index: 3)], 145 | numberOfWinners: 2, 146 | winnerIndexes: [0, 1] 147 | ) 148 | ] 149 | ) 150 | 151 | expect(bracket == expectedBracket).to(beTrue()) 152 | } 153 | 154 | it("returns child builder for matchPath") { 155 | let builder1 = TournamentBuilder(numberOfWinners: 2, winnerIndexes: [1, 2]).addEntry(3) 156 | let builder2 = TournamentBuilder(numberOfLayers: 3).addEntry() 157 | let builder = TournamentBuilder() 158 | .addBracket { builder1 } 159 | .addBracket { .init(numberOfLayers: 4) } 160 | .addBracket { 161 | TournamentBuilder(numberOfLayers: 2) 162 | .addEntry() 163 | .addBracket { builder2 } 164 | } 165 | 166 | expect(builder.getChildBuilder(for: .init(layer: 5, item: 0))).to(be(builder)) 167 | expect(builder.getChildBuilder(for: .init(layer: 4, item: 0))).to(be(builder1)) 168 | expect(builder.getChildBuilder(for: .init(layer: 3, item: 4))).to(be(builder2)) 169 | expect(builder.getChildBuilder(for: .init(layer: 5, item: 1))).to(beNil()) 170 | expect(builder.getChildBuilder(for: .init(layer: 6, item: 0))).to(beNil()) 171 | expect(builder.getChildBuilder(for: .init(layer: 4, item: 20))).to(beNil()) 172 | } 173 | 174 | // Int extension ------------ 175 | 176 | it("has divisors") { 177 | expect((-5).divisors).to(equal([])) 178 | expect(0.divisors).to(equal([])) 179 | expect(1.divisors).to(equal([1])) 180 | expect(2.divisors).to(equal([1])) 181 | expect(3.divisors).to(equal([1])) 182 | expect(4.divisors).to(equal([1, 2])) 183 | expect(9.divisors).to(equal([1, 3])) 184 | expect(24.divisors).to(equal([1, 2, 3, 4, 6, 8, 12])) 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /KRTournamentViewTests/Tests/TournamentInfoTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TournamentInfoTests.swift 3 | // KRTournamentView 4 | // 5 | // Copyright © 2019 Krimpedance. All rights reserved. 6 | // 7 | // swiftlint:disable superfluous_disable_command 8 | // swiftlint:disable identifier_name 9 | // swiftlint:disable force_try 10 | // swiftlint:disable force_cast 11 | // swiftlint:disable function_body_length 12 | 13 | import XCTest 14 | import Quick 15 | import Nimble 16 | @testable import KRTournamentView 17 | 18 | class TournamentInfoTests: QuickSpec { 19 | typealias InitializerDataset = ( 20 | numberOfLayer: Int, 21 | style: KRTournamentViewStyle, 22 | drawHalf: DrawHalf, 23 | rect: CGRect, 24 | firstEntryNum: Int, 25 | secondEntryNum: Int, 26 | entrySize: CGSize, 27 | expectedInfo: TournamentInfo 28 | ) 29 | 30 | typealias EntryPointDataset = ( 31 | style: KRTournamentViewStyle, 32 | drawHalf: DrawHalf, 33 | rectSize: CGSize, 34 | firstEntryNum: Int, 35 | secondEntryNum: Int, 36 | stepSize: CGSize, 37 | drawMargin: CGFloat, 38 | expectedPoints: [CGPoint?] 39 | ) 40 | 41 | override func spec() { 42 | it("can initialize") { 43 | let info = TournamentInfo( 44 | numberOfLayer: 2, 45 | style: .left, 46 | drawHalf: .first, 47 | rect: .init(x: 0, y: 0, width: 360, height: 360), 48 | firstEntryNum: 4, 49 | secondEntryNum: 0, 50 | entrySize: .init(width: 80, height: 100), 51 | stepSize: .init(width: 80, height: 40), 52 | drawMargin: 50 53 | ) 54 | 55 | expect(info.numberOfLayer).to(equal(2)) 56 | expect(info.style).to(equal(.left)) 57 | expect(info.drawHalf).to(equal(.first)) 58 | expect(info.rect).to(equal(.init(x: 0, y: 0, width: 360, height: 360))) 59 | expect(info.firstEntryNum).to(equal(4)) 60 | expect(info.secondEntryNum).to(equal(0)) 61 | expect(info.entrySize).to(equal(.init(width: 80, height: 100))) 62 | expect(info.stepSize).to(equal(.init(width: 80, height: 40))) 63 | expect(info.drawMargin).to(equal(50)) 64 | } 65 | 66 | it("can initialize with Bracket") { 67 | let info = TournamentInfo( 68 | structure: TournamentBuilder.build(numberOfLayers: 3), 69 | style: .topBottom, 70 | drawHalf: .first, 71 | rect: .init(x: 0, y: 0, width: 500, height: 500), 72 | entrySize: .init(width: 30, height: 50) 73 | ) 74 | 75 | let expectedInfo = TournamentInfo( 76 | numberOfLayer: 3, 77 | style: .topBottom, 78 | drawHalf: .first, 79 | rect: .init(x: 0, y: 0, width: 500, height: 500), 80 | firstEntryNum: 4, 81 | secondEntryNum: 4, 82 | entrySize: .init(width: 30, height: 50) 83 | ) 84 | 85 | expect(info == expectedInfo).to(beTrue()) 86 | } 87 | 88 | let listForInitializer: [InitializerDataset] = [ 89 | // entrySize is in range 90 | ( 91 | numberOfLayer: 2, 92 | style: .right, 93 | drawHalf: .second, 94 | rect: .init(x: 0, y: 0, width: 360, height: 330), 95 | firstEntryNum: 0, 96 | secondEntryNum: 4, 97 | entrySize: .init(width: 80, height: 30), 98 | expectedInfo: TournamentInfo( 99 | numberOfLayer: 2, 100 | style: .right, 101 | drawHalf: .second, 102 | rect: .init(x: 0, y: 0, width: 360, height: 330), 103 | firstEntryNum: 0, 104 | secondEntryNum: 4, 105 | entrySize: .init(width: 80, height: 30), 106 | stepSize: .init(width: 120, height: 100), 107 | drawMargin: 15 108 | ) 109 | ), 110 | ( 111 | numberOfLayer: 2, 112 | style: .top, 113 | drawHalf: .first, 114 | rect: .init(x: 0, y: 0, width: 330, height: 360), 115 | firstEntryNum: 4, 116 | secondEntryNum: 0, 117 | entrySize: .init(width: 30, height: 80), 118 | expectedInfo: TournamentInfo( 119 | numberOfLayer: 2, 120 | style: .top, 121 | drawHalf: .first, 122 | rect: .init(x: 0, y: 0, width: 330, height: 360), 123 | firstEntryNum: 4, 124 | secondEntryNum: 0, 125 | entrySize: .init(width: 30, height: 80), 126 | stepSize: .init(width: 100, height: 120), 127 | drawMargin: 15 128 | ) 129 | ), 130 | // entrySize is out range 131 | ( 132 | numberOfLayer: 2, 133 | style: .left, 134 | drawHalf: .first, 135 | rect: .init(x: 0, y: 0, width: 360, height: 360), 136 | firstEntryNum: 4, 137 | secondEntryNum: 0, 138 | entrySize: .init(width: 80, height: 100), 139 | expectedInfo: TournamentInfo( 140 | numberOfLayer: 2, 141 | style: .left, 142 | drawHalf: .first, 143 | rect: .init(x: 0, y: 0, width: 360, height: 360), 144 | firstEntryNum: 4, 145 | secondEntryNum: 0, 146 | entrySize: .init(width: 80, height: 90), 147 | stepSize: .init(width: 120, height: 90), 148 | drawMargin: 45 149 | ) 150 | ), 151 | ( 152 | numberOfLayer: 2, 153 | style: .bottom, 154 | drawHalf: .second, 155 | rect: .init(x: 0, y: 0, width: 360, height: 360), 156 | firstEntryNum: 0, 157 | secondEntryNum: 4, 158 | entrySize: .init(width: 100, height: 80), 159 | expectedInfo: TournamentInfo( 160 | numberOfLayer: 2, 161 | style: .bottom, 162 | drawHalf: .second, 163 | rect: .init(x: 0, y: 0, width: 360, height: 360), 164 | firstEntryNum: 0, 165 | secondEntryNum: 4, 166 | entrySize: .init(width: 90, height: 80), 167 | stepSize: .init(width: 90, height: 120), 168 | drawMargin: 45 169 | ) 170 | ), 171 | ( 172 | numberOfLayer: 3, 173 | style: .leftRight, 174 | drawHalf: .first, 175 | rect: .init(x: 0, y: 0, width: 360, height: 360), 176 | firstEntryNum: 4, 177 | secondEntryNum: 3, 178 | entrySize: .init(width: 60, height: 100), 179 | expectedInfo: TournamentInfo( 180 | numberOfLayer: 3, 181 | style: .leftRight, 182 | drawHalf: .first, 183 | rect: .init(x: 0, y: 0, width: 360, height: 360), 184 | firstEntryNum: 4, 185 | secondEntryNum: 3, 186 | entrySize: .init(width: 60, height: 90), 187 | stepSize: .init(width: 60, height: 90), 188 | drawMargin: 45 189 | ) 190 | ), 191 | ( 192 | numberOfLayer: 3, 193 | style: .topBottom, 194 | drawHalf: .second, 195 | rect: .init(x: 0, y: 0, width: 360, height: 360), 196 | firstEntryNum: 5, 197 | secondEntryNum: 4, 198 | entrySize: .init(width: 100, height: 60), 199 | expectedInfo: TournamentInfo( 200 | numberOfLayer: 3, 201 | style: .topBottom, 202 | drawHalf: .second, 203 | rect: .init(x: 0, y: 0, width: 360, height: 360), 204 | firstEntryNum: 5, 205 | secondEntryNum: 4, 206 | entrySize: .init(width: 90, height: 60), 207 | stepSize: .init(width: 90, height: 60), 208 | drawMargin: 45 209 | ) 210 | ) 211 | ] 212 | 213 | listForInitializer.forEach { dataset in 214 | context("\(dataset)") { 215 | it("can initialize after validate") { 216 | let info = TournamentInfo( 217 | numberOfLayer: dataset.numberOfLayer, 218 | style: dataset.style, 219 | drawHalf: dataset.drawHalf, 220 | rect: dataset.rect, 221 | firstEntryNum: dataset.firstEntryNum, 222 | secondEntryNum: dataset.secondEntryNum, 223 | entrySize: dataset.entrySize 224 | ) 225 | 226 | expect(info == dataset.expectedInfo).to(beTrue()) 227 | } 228 | } 229 | } 230 | 231 | let listForEntryPoint: [EntryPointDataset] = [ 232 | ( 233 | .left, .first, .init(width: 100, height: 200), 2, 3, .init(width: 10, height: 20), 10, 234 | [.init(x: 0, y: 10), .init(x: 0, y: 30)] 235 | ), 236 | ( 237 | .right, .second, .init(width: 100, height: 200), 3, 2, .init(width: 10, height: 20), 10, 238 | [.init(x: 100, y: 10), .init(x: 100, y: 30)] 239 | ), 240 | ( 241 | .top, .first, .init(width: 100, height: 200), 2, 3, .init(width: 10, height: 20), 10, 242 | [.init(x: 10, y: 0), .init(x: 20, y: 0)] 243 | ), 244 | ( 245 | .bottom, .second, .init(width: 100, height: 200), 3, 2, .init(width: 10, height: 20), 10, 246 | [.init(x: 10, y: 200), .init(x: 20, y: 200)] 247 | ), 248 | ( 249 | .leftRight, .first, .init(width: 100, height: 200), 2, 3, .init(width: 10, height: 20), 10, 250 | [.init(x: 0, y: 10), .init(x: 0, y: 30)] 251 | ), 252 | ( 253 | .leftRight, .second, .init(width: 100, height: 200), 2, 3, .init(width: 10, height: 20), 10, 254 | [nil, nil, .init(x: 100, y: 10), .init(x: 100, y: 30), .init(x: 100, y: 50)] 255 | ), 256 | ( 257 | .topBottom, .first, .init(width: 100, height: 200), 2, 3, .init(width: 10, height: 20), 10, 258 | [.init(x: 10, y: 0), .init(x: 20, y: 0)] 259 | ), 260 | ( 261 | .topBottom, .second, .init(width: 100, height: 200), 2, 3, .init(width: 10, height: 20), 10, 262 | [nil, nil, .init(x: 10, y: 200), .init(x: 20, y: 200), .init(x: 30, y: 200)] 263 | ), 264 | ( 265 | .leftRight, .first, .init(width: 100, height: 200), 1, 2, .init(width: 10, height: 20), 10, 266 | [.init(x: 0, y: 100)] 267 | ), 268 | ( 269 | .leftRight, .second, .init(width: 100, height: 200), 2, 1, .init(width: 10, height: 20), 10, 270 | [nil, nil, .init(x: 100, y: 100)] 271 | ) 272 | ] 273 | 274 | listForEntryPoint.forEach { dataset in 275 | context("\(dataset)") { 276 | it("returns entryPoint at index") { 277 | let info = TournamentInfo( 278 | style: dataset.style, 279 | drawHalf: dataset.drawHalf, 280 | rectSize: dataset.rectSize, 281 | firstEntryNum: dataset.firstEntryNum, 282 | secondEntryNum: dataset.secondEntryNum, 283 | stepSize: dataset.stepSize, 284 | drawMargin: dataset.drawMargin 285 | ) 286 | 287 | dataset.expectedPoints.enumerated().forEach { index, point in 288 | guard let point = point else { return } 289 | expect(info.entryPoint(at: index)).to(equal(point)) 290 | } 291 | } 292 | } 293 | } 294 | 295 | it("returns TournamentInfo converted drawHalf") { 296 | let info = TournamentInfo( 297 | numberOfLayer: 3, 298 | style: .topBottom, 299 | drawHalf: .first, 300 | rect: .init(x: 0, y: 0, width: 500, height: 500), 301 | firstEntryNum: 4, 302 | secondEntryNum: 4, 303 | entrySize: .init(width: 30, height: 50) 304 | ) 305 | 306 | let convertedIno = info.convert(drawHalf: .second) 307 | 308 | let expectedInfo = TournamentInfo( 309 | numberOfLayer: 3, 310 | style: .topBottom, 311 | drawHalf: .second, 312 | rect: .init(x: 0, y: 0, width: 500, height: 500), 313 | firstEntryNum: 4, 314 | secondEntryNum: 4, 315 | entrySize: .init(width: 30, height: 50) 316 | ) 317 | 318 | expect(convertedIno == expectedInfo).to(beTrue()) 319 | } 320 | } 321 | } 322 | 323 | // MARK: - TournamentInfo extensions ------------ 324 | 325 | private extension TournamentInfo { 326 | init( 327 | style: KRTournamentViewStyle, 328 | drawHalf: DrawHalf, 329 | rectSize: CGSize, 330 | firstEntryNum: Int, 331 | secondEntryNum: Int, 332 | stepSize: CGSize, 333 | drawMargin: CGFloat 334 | ) { 335 | self.init( 336 | numberOfLayer: 0, 337 | style: style, 338 | drawHalf: drawHalf, 339 | rect: .init(origin: .zero, size: rectSize), 340 | firstEntryNum: firstEntryNum, 341 | secondEntryNum: secondEntryNum, 342 | entrySize: .zero, 343 | stepSize: stepSize, 344 | drawMargin: drawMargin 345 | ) 346 | } 347 | 348 | static func == (lhs: TournamentInfo, rhs: TournamentInfo) -> Bool { 349 | return lhs.numberOfLayer == rhs.numberOfLayer 350 | && lhs.style == rhs.style 351 | && lhs.drawHalf == rhs.drawHalf 352 | && lhs.rect == rhs.rect 353 | && lhs.firstEntryNum == rhs.firstEntryNum 354 | && lhs.secondEntryNum == rhs.secondEntryNum 355 | && lhs.entrySize == rhs.entrySize 356 | && lhs.stepSize == rhs.stepSize 357 | && lhs.drawMargin == rhs.drawMargin 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /KRTournamentViewTests/Tests/TournamentStructureTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TournamentStructureTests.swift 3 | // KRTournamentView 4 | // 5 | // Copyright © 2019 Krimpedance. All rights reserved. 6 | // 7 | // swiftlint:disable superfluous_disable_command 8 | // swiftlint:disable identifier_name 9 | // swiftlint:disable force_try 10 | // swiftlint:disable force_cast 11 | // swiftlint:disable function_body_length 12 | 13 | import XCTest 14 | import Quick 15 | import Nimble 16 | @testable import KRTournamentView 17 | 18 | class TournamentStructureTests: QuickSpec { 19 | private struct FakeBracket: TournamentStructure {} 20 | 21 | override func spec() { 22 | it("returns entries") { 23 | let items: [TournamentStructure] = [ 24 | Entry(index: 4), 25 | Bracket(children: [Entry(index: 1), Entry(index: 2)]), 26 | Bracket(children: [ 27 | Bracket(children: [Entry(index: 1), Entry(index: 2)]), 28 | Bracket(children: [Entry(index: 4), Entry(index: 5)]) 29 | ]), 30 | FakeBracket() 31 | ] 32 | 33 | let expectedList: [(KRTournamentViewStyle, DrawHalf, [[Entry]])] = [ 34 | ( 35 | .left, .first, 36 | [ 37 | [Entry(index: 4)], 38 | [Entry(index: 1), Entry(index: 2)], 39 | [Entry(index: 1), Entry(index: 2), Entry(index: 4), Entry(index: 5)], 40 | [] 41 | ] 42 | ), 43 | ( 44 | .bottom, .second, 45 | [ 46 | [Entry(index: 4)], 47 | [Entry(index: 1), Entry(index: 2)], 48 | [Entry(index: 1), Entry(index: 2), Entry(index: 4), Entry(index: 5)], 49 | [] 50 | ] 51 | ), 52 | ( 53 | .leftRight, .first, 54 | [ 55 | [Entry(index: 4)], 56 | [Entry(index: 1)], 57 | [Entry(index: 1), Entry(index: 2)], 58 | [] 59 | ] 60 | ), 61 | ( 62 | .topBottom, .second, 63 | [ 64 | [Entry(index: 4)], 65 | [Entry(index: 2)], 66 | [Entry(index: 4), Entry(index: 5)], 67 | [] 68 | ] 69 | ) 70 | ] 71 | 72 | expectedList.forEach { style, half, expectedEntries in 73 | items.enumerated().forEach { index, item in 74 | let entries = item.entries(style: style, drawHalf: half) 75 | expect(entries == expectedEntries[index]).to(beTrue()) 76 | } 77 | } 78 | } 79 | 80 | it("can compare") { 81 | let entry1 = Entry() 82 | let entry2 = Entry(index: 1) 83 | let entry3 = Entry(index: 2) 84 | let bracket1 = Bracket(matchPath: .init(layer: 1, item: 1), children: [entry1, entry2]) 85 | let bracket2 = Bracket(children: [entry1, entry2]) 86 | let bracket3 = Bracket(children: [entry1, entry2, entry3]) 87 | let bracket4 = Bracket(children: [entry1, entry2], numberOfWinners: 2) 88 | let bracket5 = Bracket(children: [entry1, entry2], winnerIndexes: [1, 2]) 89 | 90 | expect(entry1 == entry1).to(beTrue()) 91 | expect(entry2 == entry2).to(beTrue()) 92 | expect(entry1 == entry2).to(beFalse()) 93 | expect(entry2 == entry3).to(beFalse()) 94 | 95 | expect(entry1 == bracket1).to(beFalse()) 96 | 97 | expect(bracket1 == bracket1).to(beTrue()) 98 | expect(bracket1 == bracket2).to(beFalse()) 99 | expect(bracket2 == bracket3).to(beFalse()) 100 | expect(bracket2 == bracket4).to(beFalse()) 101 | expect(bracket2 == bracket5).to(beFalse()) 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 krimpedance 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "KRTournamentView", 7 | platforms: [.iOS(.v8)], 8 | products: [ 9 | .library( 10 | name: "KRTournamentView", 11 | targets: ["KRTournamentView"]), 12 | ], 13 | dependencies: [], 14 | targets: [ 15 | .target( 16 | name: "KRTournamentView", 17 | dependencies: [], 18 | path: "KRTournamentView/Classes" 19 | ), 20 | .testTarget( 21 | name: "KRTournamentViewTests", 22 | dependencies: ["KRTournamentView"], 23 | path: "KRTournamentViewTests" 24 | ), 25 | ] 26 | ) 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [日本語](./README_Ja.md) 2 | 3 | 4 | 5 | [![Version](https://img.shields.io/cocoapods/v/KRTournamentView.svg?style=flat)](https://cocoapods.org/pods/KRTournamentView) 6 | [![License](https://img.shields.io/cocoapods/l/KRTournamentView.svg?style=flat)](https://cocoapods.org/pods/KRTournamentView) 7 | [![Platform](https://img.shields.io/cocoapods/p/KRTournamentView.svg?style=flat)](https://cocoapods.org/pods/KRTournamentView) 8 | [![Download](https://img.shields.io/cocoapods/dt/KRTournamentView.svg?style=flat)](https://cocoapods.org/pods/KRTournamentView) 9 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 10 | [![CI Status](https://img.shields.io/travis/krimpedance/KRTournamentView.svg?style=flat)](https://travis-ci.org/krimpedance/KRTournamentView) 11 | 12 | `KRTournamentView` is a flexible tournament bracket that can correspond to the various structure. 13 | 14 | 15 | 16 | 17 | ## Requirements 18 | - iOS 8.0+ 19 | - Xcode 11.0+ 20 | - Swift 5.0+ 21 | 22 | 23 | ## DEMO 24 | To run the example project, clone the repo, and open `KRTournamentViewDemo.xcodeproj` from the DEMO directory. 25 | 26 | or [appetize.io](https://appetize.io/app/yevb2mx7ea7p10cjqb9fp3tenm?device=iphonex&scale=75&orientation=portrait&osVersion=13.3) 27 | 28 | 29 | ## Installation 30 | KRTournamentView can be installed in several ways. 31 | 32 | + [Swift Package Manager](https://swift.org/package-manager) 33 | 34 | `File` -> `Swift Packages` -> `Add Package Dependency` on Xcode.app 35 | 36 | + [CocoaPods](http://cocoapods.org) 37 | 38 | ```ruby 39 | # Podfile 40 | pod "KRTournamentView" 41 | ``` 42 | 43 | + [Carthage](https://github.com/Carthage/Carthage) 44 | 45 | ```ruby 46 | # Cartfile 47 | github "Krimpedance/KRTournamentView" 48 | ``` 49 | 50 | 51 | ## Documentation 52 | 53 | See [here](./Documentation/En). 54 | 55 | 56 | ## Release Note 57 | 58 | + 2.0.0 : 59 | - Update for Swift 5. 60 | - Multi entries and multi winners. 61 | 62 | + 1.1.0 : 63 | - Compatible with Swift 4.2. 64 | 65 | 66 | ## Contributing to this project 67 | I'm seeking bug reports and feature requests. 68 | 69 | 70 | ## License 71 | KRTournamentView is available under the MIT license. 72 | 73 | See the LICENSE file for more info. 74 | -------------------------------------------------------------------------------- /README_Ja.md: -------------------------------------------------------------------------------- 1 | [English](./README.md) 2 | 3 | 4 | 5 | [![Version](https://img.shields.io/cocoapods/v/KRTournamentView.svg?style=flat)](https://cocoapods.org/pods/KRTournamentView) 6 | [![License](https://img.shields.io/cocoapods/l/KRTournamentView.svg?style=flat)](https://cocoapods.org/pods/KRTournamentView) 7 | [![Platform](https://img.shields.io/cocoapods/p/KRTournamentView.svg?style=flat)](https://cocoapods.org/pods/KRTournamentView) 8 | [![Download](https://img.shields.io/cocoapods/dt/KRTournamentView.svg?style=flat)](https://cocoapods.org/pods/KRTournamentView) 9 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 10 | [![CI Status](https://img.shields.io/travis/krimpedance/KRTournamentView.svg?style=flat)](https://travis-ci.org/krimpedance/KRTournamentView) 11 | 12 | `KRTournamentView` は様々な構造に対応できる柔軟なトーナメント表です. 13 | 14 | 15 | 16 | 17 | ## 必要環境 18 | - iOS 8.0+ 19 | - Xcode 11.0+ 20 | - Swift 5.0+ 21 | 22 | 23 | ## デモ 24 | `DEMO/`以下にあるサンプルプロジェクトから確認してください. 25 | 26 | または,[Appetize.io](https://appetize.io/app/yevb2mx7ea7p10cjqb9fp3tenm?device=iphonex&scale=75&orientation=portrait&osVersion=13.3)にてシュミレートしてください. 27 | 28 | 29 | ## インストール 30 | いくつかの方法で KRTournamentView をインストールすることができます. 31 | 32 | + [Swift Package Manager](https://swift.org/package-manager) 33 | 34 | `File` -> `Swift Packages` -> `Add Package Dependency` on Xcode.app 35 | 36 | + [CocoaPods](http://cocoapods.org) 37 | 38 | ```ruby 39 | # Podfile 40 | pod "KRTournamentView" 41 | ``` 42 | 43 | + [Carthage](https://github.com/Carthage/Carthage) 44 | 45 | ```ruby 46 | # Cartfile 47 | github "Krimpedance/KRTournamentView" 48 | ``` 49 | 50 | 51 | ## ドキュメント 52 | 53 | [こちら](./Documentation/Ja)にまとめてあります. 54 | 55 | 56 | ## リリースノート 57 | 58 | + 2.0.0 : 59 | - Swift 5 に対応. 60 | - 複数人対戦, 複数人勝ち上がりに対応 61 | 62 | + 1.1.0 : 63 | - Swift 4.2 に対応. 64 | 65 | 66 | ## ライブラリに関する質問等 67 | バグや機能のリクエストがありましたら,気軽にコメントしてください. 68 | 69 | 70 | ## ライセンス 71 | KRTournamentViewはMITライセンスに準拠しています. 72 | 73 | 詳しくは`LICENSE`ファイルをみてください. 74 | --------------------------------------------------------------------------------