Skip to content

Commit 4dc15d1

Browse files
authored
Merge pull request #57 from 3sidedcube/release/v2.0.0
Release/v2.0.0
2 parents 18b77e4 + 0869616 commit 4dc15d1

File tree

15 files changed

+797
-190
lines changed

15 files changed

+797
-190
lines changed

.DS_Store

-8 KB
Binary file not shown.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ DerivedData
1616
*.hmap
1717
*.ipa
1818
*.xcuserstate
19+
.DS_Store
1920

2021
# CocoaPods
2122
#

IBMigrationTool/IBMigrationTool

91.1 KB
Binary file not shown.
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
//
2+
// InterfaceBuilderFileMigrator.swift
3+
// ThunderBasics
4+
//
5+
// Created by Simon Mitchell on 18/09/2020.
6+
// Copyright © 2020 threesidedcube. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
/// A class responsible for migrating an Interface Builder to fix issues
12+
/// when upgrading to `ThunderBasics` v2.0.0
13+
final class InterfaceBuilderFileMigrator {
14+
15+
static let directlyMappableAttributes: [(String, String)] = [
16+
("color", "borderColor"),
17+
("number", "borderWidth"),
18+
("number", "cornerRadius"),
19+
("color", "shadowColor"),
20+
("number", "shadowOpacity"),
21+
("number", "shadowRadius")
22+
]
23+
24+
static let removedCustomClasses: [String] = [
25+
"TSCTextView",
26+
"TSCView",
27+
"TSCImageView"
28+
]
29+
30+
/// Represents the current contents of the interface builder file
31+
///
32+
/// - Note: This will be changed by calling the `migrate` function to
33+
/// represent the fixed version of the file!
34+
var string: String
35+
36+
convenience init?(filePath: String) {
37+
let fileURL = URL(fileURLWithPath: filePath)
38+
guard let data = try? Data(contentsOf: fileURL) else {
39+
return nil
40+
}
41+
guard let fileString = String(data: data, encoding: .utf8) else {
42+
return nil
43+
}
44+
self.init(string: fileString)
45+
}
46+
47+
init(string: String) {
48+
self.string = string
49+
}
50+
51+
/// Performs migration of the current file, storing the result in `string`
52+
func migrate() {
53+
migrateUserDefinedRuntimeAttributes()
54+
migrateRemovedCustomClasses()
55+
}
56+
57+
private func migrateRemovedCustomClasses() {
58+
59+
Self.removedCustomClasses.forEach { (customClass) in
60+
61+
// Have to catch all of this in one redux as could appear in either order but if we do one check before the other
62+
// we may miss/break other cases!
63+
//
64+
// check for customModule="ThunderBasics" ... "customClass"=customClass || "customClass"=customClass ... customModule="ThunderBasics"
65+
string = string.replacingOccurrences(
66+
of: "(customModule=\"ThunderBasics\"([^>]*))?\\s*customClass=\"\(customClass)\"\\s*(([^>]*)customModule=\"ThunderBasics\")?",
67+
with: "$2$4", // We will only either have $2 or $4 so we don't need a space between them!
68+
options: .regularExpression,
69+
range: nil
70+
)
71+
}
72+
}
73+
74+
private func migrateUserDefinedRuntimeAttributes() {
75+
76+
// These use the same type as `CALayer` equivalents, so can be
77+
// mapped simply using the same regex
78+
Self.directlyMappableAttributes.forEach { (attribute) in
79+
80+
string = string.replacingOccurrences(
81+
of: "<userDefinedRuntimeAttribute(\\s+)type=\"\(attribute.0)\"(\\s+)keyPath=\"(\(attribute.1))\">",
82+
with: "<userDefinedRuntimeAttribute$1type=\"\(attribute.0)\"$2keyPath=\"layer.$3\">",
83+
options: .regularExpression,
84+
range: nil
85+
)
86+
}
87+
88+
// shadowOffset is a bit more complex because we used `CGPoint` but
89+
// `CALayer` uses `CGSize`
90+
91+
// Note this regex isn't perfect as it allows for x,y to take the form 1.2.1.3.1.2 e.t.c
92+
// however this should be impossible to enter in Interface Builder and it makes replacing it
93+
// far simpler so we will leave be for now!
94+
string = string.replacingOccurrences(
95+
of: "<userDefinedRuntimeAttribute(\\s+)type=\"point\"(\\s+)keyPath=\"shadowOffset\">(\\s+)(<point(\\s)+key=\"value\"(\\s)+x=\"([.\\d]+)\"(\\s+)y=\"([.\\d]+)\"\\/>)",
96+
with: "<userDefinedRuntimeAttribute$1type=\"size\"$2keyPath=\"layer.shadowOffset\">$3<size$5key=\"value\"$6width=\"$7\"$8height=\"$9\"\\/>",
97+
options: .regularExpression,
98+
range: nil
99+
)
100+
}
101+
}

IBMigrationTool/main.swift

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
//
2+
// main.swift
3+
// IBMigrationTool
4+
//
5+
// Created by Simon Mitchell on 17/09/2020.
6+
// Copyright © 2020 threesidedcube. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
extension FileManager {
12+
13+
/// Recurses the given directory for files with a given extension
14+
/// - Parameters:
15+
/// - type: The extension to find files for
16+
/// - directory: The directory to search in
17+
/// - Returns: An array of file paths
18+
func recursivePathsForResource(_ type: String?, directory: String) -> [String] {
19+
20+
var filePaths: [String] = []
21+
let fileEnumerator = enumerator(atPath: directory)
22+
23+
while let element = fileEnumerator?.nextObject() as? NSString {
24+
25+
if type == nil || element.pathExtension == type {
26+
filePaths.append(directory + "/" + (element as String))
27+
}
28+
}
29+
30+
return filePaths
31+
}
32+
}
33+
34+
/// Converts all `UIView+Displayable` runtime attributes to use the
35+
/// `CALayer` equivalents in the file at the given path
36+
/// - Parameter path: The file path to convert
37+
func migrateUserDefinedRuntimeAttributesInInterfaceBuilderFile(at path: String) {
38+
39+
print("Migrating user defined runtime attributes in \(path)")
40+
41+
guard let migrator = InterfaceBuilderFileMigrator(filePath: path) else {
42+
print("Failed to read data as string from: \(path)")
43+
return
44+
}
45+
migrator.migrate()
46+
47+
// Save file to disk!
48+
let newData = migrator.string.data(using: .utf8)
49+
do {
50+
let fileURL = URL(fileURLWithPath: path)
51+
try newData?.write(to: fileURL)
52+
print("Wrote migrated file to \(path)")
53+
} catch {
54+
print("Failed to write migrated file to \(path)")
55+
}
56+
}
57+
58+
print("This tool will make changes to the Interface Builder (.xib/.storyboard) files in the chosen directory. Please make sure you have no changes in your index before continuing")
59+
print("Please enter the file path to the Project you want to migrate Interface Builder files to ThunderBasics 2.0.0")
60+
var filePath = readLine(strippingNewline: true)
61+
while filePath == nil {
62+
filePath = readLine(strippingNewline: true)
63+
}
64+
65+
filePath = filePath?.trimmingCharacters(in: .whitespaces).replacingOccurrences(of: "\\ ", with: " ")
66+
print("Parsing contents of \(filePath!) for IB Files")
67+
68+
FileManager.default.recursivePathsForResource("xib", directory: filePath!).forEach { (xibPath) in
69+
migrateUserDefinedRuntimeAttributesInInterfaceBuilderFile(at: xibPath)
70+
}
71+
72+
FileManager.default.recursivePathsForResource("storyboard", directory: filePath!).forEach { (storyboardPath) in
73+
migrateUserDefinedRuntimeAttributesInInterfaceBuilderFile(at: storyboardPath)
74+
}
75+
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
//
2+
// IBMigrationToolTests.swift
3+
// IBMigrationToolTests
4+
//
5+
// Created by Simon Mitchell on 18/09/2020.
6+
// Copyright © 2020 threesidedcube. All rights reserved.
7+
//
8+
9+
import XCTest
10+
11+
class IBMigrationToolTests: XCTestCase {
12+
13+
override func setUpWithError() throws {
14+
// Put setup code here. This method is called before the invocation of each test method in the class.
15+
}
16+
17+
override func tearDownWithError() throws {
18+
// Put teardown code here. This method is called after the invocation of each test method in the class.
19+
}
20+
21+
func testStoryboardFileMigratesCorrectly() {
22+
23+
// May not be .storyboard (Can't include iOS storyboard in macOS test target), but the contents are the same!
24+
guard let initialPath = Bundle.init(for: IBMigrationToolTests.self).url(forResource: "TestStoryboard", withExtension: "xml") else {
25+
XCTFail("Missing TestStoryboard.xml in test target!")
26+
return
27+
}
28+
guard let finalPath = Bundle.init(for: IBMigrationToolTests.self).url(forResource: "TestStoryboard-Migrated", withExtension: "xml") else {
29+
XCTFail("Missing TestStoryboard-Migrated in test target!")
30+
return
31+
}
32+
guard let finalString = try? String(contentsOf: finalPath) else {
33+
XCTFail("Failed to read TestStoryboard-Migrated as string!")
34+
return
35+
}
36+
let migrator = InterfaceBuilderFileMigrator(filePath: initialPath.path)
37+
38+
XCTAssertNotNil(migrator)
39+
40+
guard let fileMigrator = migrator else { return }
41+
42+
fileMigrator.migrate()
43+
44+
// Remove whitespace when comparing because it's irrelevant and seems to cause issues when comparing!
45+
XCTAssertEqual(
46+
fileMigrator.string.replacingOccurrences(of: "\\s", with: "", options: .regularExpression, range: nil),
47+
finalString.replacingOccurrences(of: "\\s", with: "", options: .regularExpression, range: nil)
48+
)
49+
}
50+
}

IBMigrationToolTests/Info.plist

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>CFBundleDevelopmentRegion</key>
6+
<string>$(DEVELOPMENT_LANGUAGE)</string>
7+
<key>CFBundleExecutable</key>
8+
<string>$(EXECUTABLE_NAME)</string>
9+
<key>CFBundleIdentifier</key>
10+
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
11+
<key>CFBundleInfoDictionaryVersion</key>
12+
<string>6.0</string>
13+
<key>CFBundleName</key>
14+
<string>$(PRODUCT_NAME)</string>
15+
<key>CFBundlePackageType</key>
16+
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
17+
<key>CFBundleShortVersionString</key>
18+
<string>1.0</string>
19+
<key>CFBundleVersion</key>
20+
<string>1</string>
21+
</dict>
22+
</plist>

0 commit comments

Comments
 (0)