The available environment variables have this:
CI_BRANCH
The name of the source branch that Xcode Cloud checked out for the current build, for example, main.
However, this is only available for builds started by a branch change. I tried using it in a manual build, and it was just an empty string. Is there a way to get the branch name for manually started workflows? You have to choose a branch to start one, so I'd assume that info is available somewhere.
Post
Replies
Boosts
Views
Activity
I'm setting up my XCode Cloud workflows based on an existing CICD pipeline. One of the things I'd really like the workflow to do is upload an IPA file to BrowserStack, which is the tool we use for our testing. I have a curl command which will do that:
curl -u "<username>:<api key>" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@/path/to/app/file/Application-debug.ipa"
So I added this to the ci_post_xcodebuild script:
if [[ $CI_XCODEBUILD_ACTION = "archive" ]];
then
curl -u "$BROWSERSTACK_USERID:$BROWSERSTACK_APIKEY" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@$CI_ARCHIVE_PATH"
curl -u "$BROWSERSTACK_USERID:$BROWSERSTACK_APIKEY" -X POST "https://api-cloud.browserstack.com/app-automate/upload" -F "file=@$CI_ARCHIVE_PATH"
fi
But I got an "incorrect extension" error. Once I echoed the $CI_ARCHIVE_PATH, it was obvious why - it was leading to a file called build.xcarchive.
Is there any way to get access to the IPA file? There's a different curl command that will upload an IPA file from a URL, so I could try getting the URL for the build from App Store Connect? It has to be a public URL, though, so I'm not sure BrowserStack would be able to access our builds.
I'm investigating converting our pipeline that uses Gitlab runners and Fastlane to XCode Cloud.
I think I got the basic workflow set up ok, but the problem comes in when trying to archive to a different version - we generally have the next release in our develop branch, the following release in a delta branch and/or feature branches, and sometimes switch to a far-future version to test things when we don't want a build on the current release to confuse our testing team.
With our current setup, we have a Fastlane lane that checks the version in our project, checks the latest build number for that version, and increments it, like so:
lane:checkAndIncrementBuildNumber do
# Increment the build number
localProjectVersion = get_version_number_from_plist(
xcodeproj: PROJECT_NAME,
target: TARGET_NAME,
build_configuration_name: BUILD_CONFIGURATION,
plist_build_setting_support: true)
current_build_number = latest_testflight_build_number(api_key_path: API_KEY_PATH,version: localProjectVersion)
increment_build_number(
build_number: current_build_number + 1
)
incrementedBuildNumber = get_info_plist_value(path: INFO_PLIST_PATH, key: "CFBundleVersion")
File.write(VERSION_NUMBER_FILE_NAME, localProjectVersion)
File.write(BUILD_NUMBER_FILE_NAME, incrementedBuildNumber)
File.write(VERSION_BUILD_FILE_NAME, localProjectVersion + " (#{incrementedBuildNumber})")
File.write(IPA_FILE_NAME_FILE, IPA_FILE_NAME_START + localProjectVersion.tr(".","") + "b" + incrementedBuildNumber + ".ipa")
end
Can I just call this lane or something like it in the ci_pre_xcodebuild script? Or is there a way to replicate this functionality without having to set up new workflows every time we switch releases?
I'm working on converting an app to SwiftUI, and I have a menu that used to be several table cells in a storyboard, but I moved it to an embedded SwiftUI view instead.
Here's the old way (from override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell ):
cellReuseID = "BillingToolsCell"
let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseID, for: indexPath)
if let billingToolsCell = cell as? BillingToolsCell {
billingToolsCell.billingToolsOptions.text = billingTools[indexPath.row].title
// Accessibility
billingToolsCell.isAccessibilityElement = true
billingToolsCell.accessibilityIdentifier = "Billing_\(billingTools[indexPath.row].title.replacingOccurrences(of: " ", with: ""))"
}
return cell
And here's the new way I'm creating the cell:
cellReuseID = "BillingToolsSwiftUI"
if let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseID, for: indexPath) as? SwiftUIHostTableViewCell<BillingToolsView> {
let view = BillingToolsView(billingToolVM: BillingToolViewModel()) { segueID in
self.performSegue(segueID: segueID)
}
cell.host(view, parent: self)
return cell
}
Here's the swiftUI view:
struct BillingToolsView: View {
@StateObject var billingToolVM: BillingToolViewModel
var navigationCallback: (String) -> Void
var body: some View {
VStack {
VStack{
ForEach(self.billingToolVM.billingToolList, id: \.self) { tool in
Button {
navigationCallback(tool.segueID)
} label: {
BillingToolsRowView(toolName: tool.title)
Divider().foregroundColor(AFINeutral800_SwiftUI)
}
.accessibilityIdentifier("Billing_\(tool.title.replacingOccurrences(of: " ", with: ""))")
}
}
.padding(.vertical)
.padding(.leading)
.background(AFINeutral0_SwiftUI)
}
}
}
If I check the accessibility inspector, I can see the identifier - here it is showing Billing_PaymentHistory:
But when the testers try to run their tests in Appium, they don't see any identifier at all:
Did I mess up setting up the accessibility identifier somehow? Or do the testers need to update their script?
I'm having issues animating views inside a ForEach list that's inside a border. Here's my code:
struct ContentView: View {
@State var showNewMethodOptions = false
var body: some View {
VStack {
Button {
showNewMethodOptions.toggle()
} label: {
HStack {
Image(systemName: showNewMethodOptions ? "chevron.up" : "plus")
.resizable()
.scaledToFit()
.frame(width: 32)
.foregroundColor(.blue)
Text("Add new payment method")
.foregroundColor(.gray)
.padding(.leading, 6)
Spacer()
}
.padding(.horizontal)
.padding(.top, 20)
.padding(.bottom, showNewMethodOptions ? 10 : 20)
}
if showNewMethodOptions {
ForEach(PaymentMethodType.allCases) { method in
NavigationLink {
} label: {
ZStack(alignment: .center) {
RoundedRectangle(cornerRadius: 5)
.stroke(.blue)
Text(method.rawValue)
.font(.title3)
.foregroundColor(.gray)
}
.frame(height: 45.0)
.background()
.padding(.horizontal)
.padding(.vertical, 5)
.transition(.slide)
}
}
}
}
.animation(.spring, value: showNewMethodOptions)
.padding(.bottom, showNewMethodOptions ? 16 : 0)
.padding(.horizontal)
.overlay(
RoundedRectangle(cornerRadius: 4)
.stroke(.gray, lineWidth: 1)
.padding(.vertical, 4)
.padding(.horizontal)
)
}
}
enum PaymentMethodType: String, CaseIterable, Identifiable {
var id: Self {
return self
}
case checking = "Checking"
case savings = "Savings"
case creditOrDebitCard = "Card"
}
When the animation happens, the "add a new payment method" HStack animates nicely enough, but the payment method options fade in while it's still sliding up, which doesn't look great because it looks like everything's on top of each other. The options also don't seem to follow the transition animation I apply to them - it's always just a fade. Is there a way to get these elements to animate together?
I'm trying to use a Link in a swiftUI view, and like every tutorial online, I'd like to use this one:
Link(destination: URL, label: () -> View)
It pops up as one of the autocomplete options, but as soon as I add it to my code, I get these three errors:
Extra arguments at positions #1, #2 in call
Missing arguments for parameters 'rel', 'href', 'method' in call
Static method 'buildExpression' requires that 'Link' conform to 'View'
It looks like XCode wants to force me to use this Link instead:
Link(rel: String, href: String, method: String)
but I don't understand why XCode won't let me use the first one. Does anyone know why this is happening?
I'm having an issue where I have a grid with a number of items that I'm retrieving from an API. It's a SwiftUI LazyVGrid embedded in a UIKit storyboard TableView as a UITableViewCell.
The user has the ability to dismiss the items on a different screen, and then the main screen refreshes to reflect the change. Everything looks fine until the user dismisses enough items to reduce the number of rows. The items themselves still go away, but it looks like the hosting controller isn't responding to the changing height of the grid, so it leaves a weird-looking empty space.
Looking through the view hierarchy, I see that every time an item is removed, the updated grid view is stacked on top of the old view instead of replacing it, so I thought that could be causing the issue, if the constraints are still taking the old view into account.
Is there any way to stop this extra space from showing?
Here's the table cell that's hosting the SwiftUI view:
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: .default, reuseIdentifier: "opportunityGridCell")
}
func host(_ view: Content, parent: UIViewController) {
let hostingController = UIHostingController(rootView: view)
hostingController.view.backgroundColor = .lightGray
layoutIfNeeded()
parent.addChild(hostingController)
contentView.addSubview(hostingController.view)
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
hostingController.view.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
hostingController.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
hostingController.view.leftAnchor.constraint(equalTo: contentView.leftAnchor).isActive = true
hostingController.view.rightAnchor.constraint(equalTo: contentView.rightAnchor).isActive = true
hostingController.didMove(toParent: parent)
hostingController.view.layoutIfNeeded()
}
}
And the SwiftUI view:
struct OpportunityGridView: View {
@ObservedObject var opportunityViewModel: OpportunityViewModel
let goToDetailsPage: (PolicyOpportunity, OpportunityContent) -> Void
let columns = [
GridItem(.flexible(), alignment: .top),
GridItem(.flexible(), alignment: .top),
GridItem(.flexible(), alignment: .top)
]
var body: some View {
VStack(alignment: .leading) {
Spacer()
ScrollView {
LazyVGrid(columns: columns, spacing: 20) {
ForEach(opportunityViewModel.opportunities, id: \.self) { opportunity in
if let opportunityContent = opportunityViewModel.getOpportunityContent(opportunity: opportunity) {
Button(action: {
goToDetailsPage(opportunity, opportunityContent)
}, label: {
OpportunityView(opportunityImage: Image(opportunityContent.opportunityIconName), opportunityLabel: opportunityContent.cellTitle)
})
}
}
}
}
}
}
}