Skip to content

Commit

Permalink
Support generating info.plist for Watch Apps & Extensions
Browse files Browse the repository at this point in the history
Part of: tuist#628

- Added watchOS App / Extension defaults to the info plist content provider
- Watch apps reference their host applications bundle identifier in the info plist `WKCompanionAppBundleIdentifier` key
- Watch app extensions reference their host watch apps bundle identifier in the info plist `NSExtension.NSExtensionAttributes.WKAppBundleIdentifier`
- As such the parent project is now used to perform lookups for those hosts to extract their bundle identifiers
- Updated fixture to leverage generated info.plist files

Test Plan:

- run `tuist generate` within `fixtures/ios_app_with_watchapp2`
- Verify the info.plist files generated in `Derrived/InfoPlists` matche the ones created by Xcode
  (They were previously checked in under `Support`)
  • Loading branch information
kwridan committed Dec 13, 2019
1 parent b61d9a4 commit 10754e1
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 19 deletions.
4 changes: 3 additions & 1 deletion Sources/TuistGenerator/Generator/DerivedFileGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ final class DerivedFileGenerator: DerivedFileGenerating {
if case let InfoPlist.dictionary(content) = infoPlist {
dictionary = content.mapValues { $0.value }
} else if case let InfoPlist.extendingDefault(extended) = infoPlist,
let content = self.infoPlistContentProvider.content(target: target, extendedWith: extended) {
let content = self.infoPlistContentProvider.content(project: project,
target: target,
extendedWith: extended) {
dictionary = content
} else {
return
Expand Down
59 changes: 57 additions & 2 deletions Sources/TuistGenerator/Generator/InfoPlistContentProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ protocol InfoPlistContentProviding {
/// and product, and extends them with the values provided by the user.
///
/// - Parameters:
/// - project: The project that host the target for which the Info.plist content will be returned
/// - target: Target whose Info.plist content will be returned.
/// - extendedWith: Values provided by the user to extend the default ones.
/// - Returns: Content to generate the Info.plist file.
func content(target: Target, extendedWith: [String: InfoPlist.Value]) -> [String: Any]?
func content(project: Project, target: Target, extendedWith: [String: InfoPlist.Value]) -> [String: Any]?
}

final class InfoPlistContentProvider: InfoPlistContentProviding {
Expand All @@ -20,10 +21,11 @@ final class InfoPlistContentProvider: InfoPlistContentProviding {
/// and product, and extends them with the values provided by the user.
///
/// - Parameters:
/// - project: The project that host the target for which the Info.plist content will be returned
/// - target: Target whose Info.plist content will be returned.
/// - extendedWith: Values provided by the user to extend the default ones.
/// - Returns: Content to generate the Info.plist file.
func content(target: Target, extendedWith: [String: InfoPlist.Value]) -> [String: Any]? {
func content(project: Project, target: Target, extendedWith: [String: InfoPlist.Value]) -> [String: Any]? {
if target.product == .staticLibrary || target.product == .dynamicLibrary {
return nil
}
Expand All @@ -48,6 +50,20 @@ final class InfoPlistContentProvider: InfoPlistContentProviding {
extend(&content, with: macos())
}

// watchOS app
if target.product == .watch2App, target.platform == .watchOS {
let host = findHost(for: target, in: project)
extend(&content, with: watchosApp(name: target.name,
hostAppBundleId: host?.bundleId))
}

// watchOS app extension
if target.product == .watch2Extension, target.platform == .watchOS {
let host = findHost(for: target, in: project)
extend(&content, with: watchosAppExtension(name: target.name,
hostAppBundleId: host?.bundleId))
}

extend(&content, with: extendedWith.unwrappingValues())

return content
Expand Down Expand Up @@ -145,6 +161,39 @@ final class InfoPlistContentProvider: InfoPlistContentProviding {
]
}

/// Returns the default Info.plist content for a watchOS App
///
/// - Parameter hostAppBundleId: The host application's bundle identifier
private func watchosApp(name: String, hostAppBundleId: String?) -> [String: Any] {
var infoPlist: [String: Any] = [
"CFBundleDisplayName": name,
"WKWatchKitApp": true,
"UISupportedInterfaceOrientations": [
"UIInterfaceOrientationPortrait",
"UIInterfaceOrientationPortraitUpsideDown",
],
]
if let hostAppBundleId = hostAppBundleId {
infoPlist["WKCompanionAppBundleIdentifier"] = hostAppBundleId
}
return infoPlist
}

/// Returns the default Info.plist content for a watchOS App Extension
///
/// - Parameter hostAppBundleId: The host application's bundle identifier
private func watchosAppExtension(name: String, hostAppBundleId: String?) -> [String: Any] {
let extensionAttributes: [String: Any] = hostAppBundleId.map { ["WKAppBundleIdentifier": $0] } ?? [:]
return [
"CFBundleDisplayName": name,
"NSExtension": [
"NSExtensionAttributes": extensionAttributes,
"NSExtensionPointIdentifier": "com.apple.watchkit",
],
"WKExtensionDelegateClassName": "$(PRODUCT_MODULE_NAME).ExtensionDelegate",
]
}

/// Given a dictionary, it extends it with another dictionary.
///
/// - Parameters:
Expand All @@ -153,4 +202,10 @@ final class InfoPlistContentProvider: InfoPlistContentProviding {
fileprivate func extend(_ base: inout [String: Any], with: [String: Any]) {
with.forEach { base[$0.key] = $0.value }
}

private func findHost(for target: Target, in project: Project) -> Target? {
return project.targets.first {
$0.dependencies.contains(.target(name: target.name))
}
}
}
125 changes: 114 additions & 11 deletions Tests/TuistGeneratorTests/Generator/InfoPlistContentProviderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ final class InfoPlistContentProviderTests: XCTestCase {
let target = Target.test(platform: .iOS, product: .app)

// When
let got = subject.content(target: target, extendedWith: ["ExtraAttribute": "Value"])
let got = subject.content(project: .empty(),
target: target,
extendedWith: ["ExtraAttribute": "Value"])

// Then
assertEqual(got, [
Expand Down Expand Up @@ -53,7 +55,9 @@ final class InfoPlistContentProviderTests: XCTestCase {
let target = Target.test(platform: .macOS, product: .app)

// When
let got = subject.content(target: target, extendedWith: ["ExtraAttribute": "Value"])
let got = subject.content(project: .empty(),
target: target,
extendedWith: ["ExtraAttribute": "Value"])

// Then
assertEqual(got, [
Expand All @@ -79,7 +83,9 @@ final class InfoPlistContentProviderTests: XCTestCase {
let target = Target.test(platform: .macOS, product: .framework)

// When
let got = subject.content(target: target, extendedWith: ["ExtraAttribute": "Value"])
let got = subject.content(project: .empty(),
target: target,
extendedWith: ["ExtraAttribute": "Value"])

// Then
assertEqual(got, [
Expand All @@ -101,7 +107,9 @@ final class InfoPlistContentProviderTests: XCTestCase {
let target = Target.test(platform: .macOS, product: .staticLibrary)

// When
let got = subject.content(target: target, extendedWith: ["ExtraAttribute": "Value"])
let got = subject.content(project: .empty(),
target: target,
extendedWith: ["ExtraAttribute": "Value"])

// Then
XCTAssertNil(got)
Expand All @@ -112,21 +120,116 @@ final class InfoPlistContentProviderTests: XCTestCase {
let target = Target.test(platform: .macOS, product: .dynamicLibrary)

// When
let got = subject.content(target: target, extendedWith: ["ExtraAttribute": "Value"])
let got = subject.content(project: .empty(),
target: target,
extendedWith: ["ExtraAttribute": "Value"])

// Then
XCTAssertNil(got)
}

func test_contentPackageType() {
assertPackageType(subject.content(target: .test(product: .app), extendedWith: [:]), "APPL")
assertPackageType(subject.content(target: .test(product: .unitTests), extendedWith: [:]), "BNDL")
assertPackageType(subject.content(target: .test(product: .uiTests), extendedWith: [:]), "BNDL")
assertPackageType(subject.content(target: .test(product: .bundle), extendedWith: [:]), "BNDL")
assertPackageType(subject.content(target: .test(product: .framework), extendedWith: [:]), "FMWK")
assertPackageType(subject.content(target: .test(product: .staticFramework), extendedWith: [:]), "FMWK")
assertPackageType(subject.content(project: .empty(), target: .test(product: .app), extendedWith: [:]), "APPL")
assertPackageType(subject.content(project: .empty(), target: .test(product: .unitTests), extendedWith: [:]), "BNDL")
assertPackageType(subject.content(project: .empty(), target: .test(product: .uiTests), extendedWith: [:]), "BNDL")
assertPackageType(subject.content(project: .empty(), target: .test(product: .bundle), extendedWith: [:]), "BNDL")
assertPackageType(subject.content(project: .empty(), target: .test(product: .framework), extendedWith: [:]), "FMWK")
assertPackageType(subject.content(project: .empty(), target: .test(product: .staticFramework), extendedWith: [:]), "FMWK")
assertPackageType(subject.content(project: .empty(), target: .test(product: .watch2App), extendedWith: [:]), "$(PRODUCT_BUNDLE_PACKAGE_TYPE)")
}

func test_content_whenWatchOSApp() {
// Given
let watchApp = Target.test(name: "MyWatchApp",
platform: .watchOS,
product: .watch2App)
let app = Target.test(platform: .iOS,
product: .app,
bundleId: "io.tuist.my.app.id",
dependencies: [
.target(name: "MyWatchApp"),
])
let project = Project.test(targets: [
app,
watchApp,
])

// When
let got = subject.content(project: project,
target: watchApp,
extendedWith: [
"ExtraAttribute": "Value",
])

// Then
assertEqual(got, [
"CFBundleName": "$(PRODUCT_NAME)",
"CFBundleShortVersionString": "1.0",
"CFBundlePackageType": "$(PRODUCT_BUNDLE_PACKAGE_TYPE)",
"UISupportedInterfaceOrientations": [
"UIInterfaceOrientationPortrait",
"UIInterfaceOrientationPortraitUpsideDown",
],
"CFBundleIdentifier": "$(PRODUCT_BUNDLE_IDENTIFIER)",
"CFBundleInfoDictionaryVersion": "6.0",
"CFBundleVersion": "1",
"CFBundleDevelopmentRegion": "$(DEVELOPMENT_LANGUAGE)",
"CFBundleExecutable": "$(EXECUTABLE_NAME)",
"CFBundleDisplayName": "MyWatchApp",
"WKWatchKitApp": true,
"WKCompanionAppBundleIdentifier": "io.tuist.my.app.id",
"ExtraAttribute": "Value",

])
}

func test_content_whenWatchOSAppExtension() {
// Given
let watchAppExtension = Target.test(name: "MyWatchAppExtension",
platform: .watchOS,
product: .watch2Extension)
let watchApp = Target.test(platform: .watchOS,
product: .watch2App,
bundleId: "io.tuist.my.app.id.mywatchapp",
dependencies: [
.target(name: "MyWatchAppExtension"),
])
let project = Project.test(targets: [
watchApp,
watchAppExtension,
])

// When
let got = subject.content(project: project,
target: watchAppExtension,
extendedWith: [
"ExtraAttribute": "Value",
])

// Then
assertEqual(got, [
"CFBundleName": "$(PRODUCT_NAME)",
"CFBundleShortVersionString": "1.0",
"CFBundlePackageType": "$(PRODUCT_BUNDLE_PACKAGE_TYPE)",
"CFBundleIdentifier": "$(PRODUCT_BUNDLE_IDENTIFIER)",
"CFBundleInfoDictionaryVersion": "6.0",
"CFBundleVersion": "1",
"CFBundleDevelopmentRegion": "$(DEVELOPMENT_LANGUAGE)",
"CFBundleExecutable": "$(EXECUTABLE_NAME)",
"CFBundleDisplayName": "MyWatchAppExtension",
"NSExtension": [
"NSExtensionAttributes": [
"WKAppBundleIdentifier": "io.tuist.my.app.id.mywatchapp",
],
"NSExtensionPointIdentifier": "com.apple.watchkit",
],
"WKExtensionDelegateClassName": "$(PRODUCT_MODULE_NAME).ExtensionDelegate",
"ExtraAttribute": "Value",
])
}

// MARK: - Helpers

fileprivate func assertPackageType(_ lhs: [String: Any]?,
_ packageType: String?,
file: StaticString = #file,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import TuistCoreTesting
@testable import TuistGenerator

final class MockInfoPlistContentProvider: InfoPlistContentProviding {
var contentArgs: [(target: Target, extendedWith: [String: InfoPlist.Value])] = []
var contentArgs: [(project: Project, target: Target, extendedWith: [String: InfoPlist.Value])] = []
var contentStub: [String: Any]?

func content(target: Target, extendedWith: [String: InfoPlist.Value]) -> [String: Any]? {
contentArgs.append((target: target, extendedWith: extendedWith))
func content(project: Project, target: Target, extendedWith: [String: InfoPlist.Value]) -> [String: Any]? {
contentArgs.append((project: project, target: target, extendedWith: extendedWith))
return contentStub ?? [:]
}
}
6 changes: 4 additions & 2 deletions fixtures/ios_app_with_watchapp2/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ let project = Project(name: "App",
platform: .watchOS,
product: .watch2App,
bundleId: "io.tuist.App.watchkitapp",
infoPlist: "Support/WatchApp-Info.plist",
infoPlist: .default,
resources: "WatchApp/**",
dependencies: [
.target(name: "WatchAppExtension")
Expand All @@ -30,7 +30,9 @@ let project = Project(name: "App",
platform: .watchOS,
product: .watch2Extension,
bundleId: "io.tuist.App.watchkitapp.watchkitextension",
infoPlist: "Support/WatchAppExtension-Info.plist",
infoPlist: .extendingDefault(with: [
"CFBundleDisplayName": "WatchApp Extension"
]),
sources: ["WatchAppExtension/**"],
resources: ["WatchAppExtension/**/*.xcassets"],
dependencies: [
Expand Down

0 comments on commit 10754e1

Please sign in to comment.