Compare commits
No commits in common. "660974ff8f9950fdd6aa76ab7f9bcbc948b98a39" and "8f01d78146d9bfc07865448729edff4890c5b2bb" have entirely different histories.
660974ff8f
...
8f01d78146
@ -1,41 +0,0 @@
|
|||||||
# SelfieCam App Store Connect Copy
|
|
||||||
|
|
||||||
## Promotional Text (max 170)
|
|
||||||
Take better selfies in any light with a customizable screen ring light, pro camera controls, and quick timer capture.
|
|
||||||
|
|
||||||
Character count: 117 / 170
|
|
||||||
|
|
||||||
## Keywords (max 100)
|
|
||||||
selfie,camera,ring light,portrait,beauty,low light,timer,flash,creator,mirror,grid,hdr,photo editor
|
|
||||||
|
|
||||||
Character count: 99 / 100
|
|
||||||
|
|
||||||
## Description (max 4000)
|
|
||||||
SelfieCam is a professional selfie camera built for better lighting and cleaner results.
|
|
||||||
|
|
||||||
Perfect for creators, makeup artists, video calls, and anyone who wants more flattering selfies in any environment.
|
|
||||||
|
|
||||||
Core features:
|
|
||||||
- Customizable screen ring light with adjustable size, color, and brightness
|
|
||||||
- Front/back camera switching with smooth full-screen preview
|
|
||||||
- Flash controls (Off, On, Auto) plus front flash support
|
|
||||||
- Self-timer options with visual countdown
|
|
||||||
- Pinch-to-zoom and optional composition grid
|
|
||||||
- Post-capture preview with quick sharing
|
|
||||||
|
|
||||||
Pro features:
|
|
||||||
- Premium lighting presets and custom ring light colors
|
|
||||||
- HDR mode and higher photo quality options
|
|
||||||
- True Mirror mode
|
|
||||||
- Skin smoothing and advanced capture controls
|
|
||||||
- Extended timer options
|
|
||||||
|
|
||||||
Built with accessibility in mind:
|
|
||||||
- VoiceOver-friendly controls and labels
|
|
||||||
- Dynamic Type support for readable text
|
|
||||||
|
|
||||||
SelfieCam also supports iCloud settings sync, so your preferences stay consistent across devices.
|
|
||||||
|
|
||||||
Download SelfieCam and get studio-style selfie lighting straight from your screen.
|
|
||||||
|
|
||||||
Character count: 1055 / 4000
|
|
||||||
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 5.0 MiB |
|
Before Width: | Height: | Size: 3.1 MiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 2.9 MiB |
|
Before Width: | Height: | Size: 317 KiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 3.0 MiB |
|
Before Width: | Height: | Size: 309 KiB |
|
Before Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 3.3 MiB |
|
Before Width: | Height: | Size: 352 KiB |
@ -503,7 +503,7 @@
|
|||||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Selfie Cam.app/Selfie Cam";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SelfieCam.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SelfieCam";
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
@ -525,7 +525,7 @@
|
|||||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Selfie Cam.app/Selfie Cam";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SelfieCam.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SelfieCam";
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -21,11 +21,10 @@ struct RootView: View {
|
|||||||
|
|
||||||
/// Whether to show the paywall (shared between views)
|
/// Whether to show the paywall (shared between views)
|
||||||
@State private var showPaywall = false
|
@State private var showPaywall = false
|
||||||
private let isScreenshotUITest = ProcessInfo.processInfo.arguments.contains("-ui-testing-screenshots")
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
if isScreenshotUITest || hasCompletedOnboarding {
|
if hasCompletedOnboarding {
|
||||||
// Main app content
|
// Main app content
|
||||||
ContentView()
|
ContentView()
|
||||||
.preferredColorScheme(.dark)
|
.preferredColorScheme(.dark)
|
||||||
|
|||||||
@ -13,7 +13,6 @@ struct ContentView: View {
|
|||||||
@State private var settings = SettingsViewModel()
|
@State private var settings = SettingsViewModel()
|
||||||
@State private var showSettings = false
|
@State private var showSettings = false
|
||||||
@State private var showPaywall = false
|
@State private var showPaywall = false
|
||||||
private let isScreenshotUITest = ProcessInfo.processInfo.arguments.contains("-ui-testing-screenshots")
|
|
||||||
|
|
||||||
@State private var capturedPhoto: CapturedPhoto?
|
@State private var capturedPhoto: CapturedPhoto?
|
||||||
@State private var showPhotoReview = false
|
@State private var showPhotoReview = false
|
||||||
@ -33,25 +32,15 @@ struct ContentView: View {
|
|||||||
ZStack {
|
ZStack {
|
||||||
// Camera view - wrapped in EquatableView to prevent re-evaluation on settings changes
|
// Camera view - wrapped in EquatableView to prevent re-evaluation on settings changes
|
||||||
if !showPhotoReview {
|
if !showPhotoReview {
|
||||||
if isScreenshotUITest {
|
EquatableView(content: CameraContainerView(
|
||||||
ScreenshotCameraPlaceholder(
|
settings: settings,
|
||||||
showSettings: $showSettings,
|
sessionKey: cameraSessionKey,
|
||||||
ringWidth: 70.0,
|
cameraPosition: settings.cameraPosition,
|
||||||
ringColor: .white,
|
onImageCaptured: { image in
|
||||||
ringOpacity: 0.7
|
handlePhotoCaptured(image)
|
||||||
)
|
}
|
||||||
.ignoresSafeArea()
|
))
|
||||||
} else {
|
.ignoresSafeArea() // Only camera ignores safe area to fill screen
|
||||||
EquatableView(content: CameraContainerView(
|
|
||||||
settings: settings,
|
|
||||||
sessionKey: cameraSessionKey,
|
|
||||||
cameraPosition: settings.cameraPosition,
|
|
||||||
onImageCaptured: { image in
|
|
||||||
handlePhotoCaptured(image)
|
|
||||||
}
|
|
||||||
))
|
|
||||||
.ignoresSafeArea() // Only camera ignores safe area to fill screen
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Photo review overlay - handles its own safe area
|
// Photo review overlay - handles its own safe area
|
||||||
@ -78,7 +67,7 @@ struct ContentView: View {
|
|||||||
)
|
)
|
||||||
// Settings button overlay - only show when NOT in photo review mode
|
// Settings button overlay - only show when NOT in photo review mode
|
||||||
.overlay(alignment: .topTrailing) {
|
.overlay(alignment: .topTrailing) {
|
||||||
if !showPhotoReview && !isScreenshotUITest {
|
if !showPhotoReview {
|
||||||
Button {
|
Button {
|
||||||
showSettings = true
|
showSettings = true
|
||||||
} label: {
|
} label: {
|
||||||
@ -94,13 +83,6 @@ struct ContentView: View {
|
|||||||
.padding(.top, Design.Spacing.small)
|
.padding(.top, Design.Spacing.small)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.overlay(alignment: .topLeading) {
|
|
||||||
if isScreenshotUITest {
|
|
||||||
Color.clear
|
|
||||||
.frame(width: 1, height: 1)
|
|
||||||
.accessibilityIdentifier("screenshot-mode-active")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.animation(.easeInOut(duration: Design.Animation.quick), value: showPhotoReview)
|
.animation(.easeInOut(duration: Design.Animation.quick), value: showPhotoReview)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
// Initialize tracking of camera position
|
// Initialize tracking of camera position
|
||||||
@ -238,3 +220,7 @@ struct CameraContainerView: View, Equatable {
|
|||||||
.startSession()
|
.startSession()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
ContentView()
|
||||||
|
}
|
||||||
|
|||||||
@ -1,151 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
import Bedrock
|
|
||||||
|
|
||||||
struct ScreenshotCameraPlaceholder: View {
|
|
||||||
@Binding var showSettings: Bool
|
|
||||||
let ringWidth: CGFloat
|
|
||||||
let ringColor: Color
|
|
||||||
let ringOpacity: Double
|
|
||||||
|
|
||||||
init(
|
|
||||||
showSettings: Binding<Bool> = .constant(false),
|
|
||||||
ringWidth: CGFloat = 70,
|
|
||||||
ringColor: Color = .white,
|
|
||||||
ringOpacity: Double = 0.7
|
|
||||||
) {
|
|
||||||
self._showSettings = showSettings
|
|
||||||
self.ringWidth = ringWidth
|
|
||||||
self.ringColor = ringColor
|
|
||||||
self.ringOpacity = ringOpacity
|
|
||||||
}
|
|
||||||
|
|
||||||
private var backgroundImage: UIImage? {
|
|
||||||
guard let imageURL = Bundle.main.url(forResource: "image", withExtension: "png") else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return UIImage(contentsOfFile: imageURL.path)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private func cameraImageLayer(size: CGSize) -> some View {
|
|
||||||
if let backgroundImage {
|
|
||||||
Image(uiImage: backgroundImage)
|
|
||||||
.resizable()
|
|
||||||
.scaledToFill()
|
|
||||||
.frame(width: size.width, height: size.height, alignment: .center)
|
|
||||||
.clipped()
|
|
||||||
} else {
|
|
||||||
Color.black
|
|
||||||
.frame(width: size.width, height: size.height)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
GeometryReader { geometry in
|
|
||||||
let size = geometry.size
|
|
||||||
ZStack {
|
|
||||||
cameraImageLayer(size: size)
|
|
||||||
|
|
||||||
ringColor
|
|
||||||
.opacity(ringOpacity)
|
|
||||||
.ignoresSafeArea()
|
|
||||||
|
|
||||||
cameraImageLayer(size: size)
|
|
||||||
.mask {
|
|
||||||
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
|
|
||||||
.padding(ringWidth)
|
|
||||||
}
|
|
||||||
|
|
||||||
PlaceholderGridOverlay(ringWidth: ringWidth)
|
|
||||||
.allowsHitTesting(false)
|
|
||||||
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
PlaceholderTopButton(
|
|
||||||
systemImage: "gearshape.fill",
|
|
||||||
accessibilityLabel: "Settings",
|
|
||||||
action: { showSettings = true }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.padding(.top, ringWidth + Design.Spacing.medium)
|
|
||||||
.padding(.trailing, Design.Spacing.medium)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
VStack(spacing: Design.Spacing.medium) {
|
|
||||||
ZoomControlView(
|
|
||||||
zoomFactor: 1.0,
|
|
||||||
isCenterStageActive: false
|
|
||||||
)
|
|
||||||
CaptureButton(action: { })
|
|
||||||
.padding(.bottom, Design.Spacing.large)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.ignoresSafeArea()
|
|
||||||
.accessibilityIdentifier("screenshot-camera-placeholder")
|
|
||||||
.overlay(alignment: .topLeading) {
|
|
||||||
Color.clear
|
|
||||||
.frame(width: 1, height: 1)
|
|
||||||
.accessibilityIdentifier("screenshot-camera-placeholder")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct PlaceholderTopButton: View {
|
|
||||||
let systemImage: String
|
|
||||||
let accessibilityLabel: String
|
|
||||||
let action: () -> Void
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Button(action: action) {
|
|
||||||
Image(systemName: systemImage)
|
|
||||||
.font(.title3)
|
|
||||||
.foregroundStyle(.white)
|
|
||||||
.frame(width: 44, height: 44)
|
|
||||||
.background(Color.black.opacity(Design.Opacity.medium), in: Circle())
|
|
||||||
.overlay {
|
|
||||||
Circle()
|
|
||||||
.strokeBorder(Color.white.opacity(Design.Opacity.subtle), lineWidth: Design.LineWidth.thin)
|
|
||||||
}
|
|
||||||
.shadow(radius: Design.Shadow.radiusSmall)
|
|
||||||
}
|
|
||||||
.accessibilityLabel(accessibilityLabel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct PlaceholderGridOverlay: View {
|
|
||||||
let ringWidth: CGFloat
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
GeometryReader { geometry in
|
|
||||||
let insetRect = CGRect(
|
|
||||||
x: ringWidth,
|
|
||||||
y: ringWidth,
|
|
||||||
width: geometry.size.width - (ringWidth * 2),
|
|
||||||
height: geometry.size.height - (ringWidth * 2)
|
|
||||||
)
|
|
||||||
|
|
||||||
Path { path in
|
|
||||||
for index in 1...2 {
|
|
||||||
let x = insetRect.minX + (insetRect.width * CGFloat(index) / 3)
|
|
||||||
path.move(to: CGPoint(x: x, y: insetRect.minY))
|
|
||||||
path.addLine(to: CGPoint(x: x, y: insetRect.maxY))
|
|
||||||
}
|
|
||||||
|
|
||||||
for index in 1...2 {
|
|
||||||
let y = insetRect.minY + (insetRect.height * CGFloat(index) / 3)
|
|
||||||
path.move(to: CGPoint(x: insetRect.minX, y: y))
|
|
||||||
path.addLine(to: CGPoint(x: insetRect.maxX, y: y))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.stroke(Color.white.opacity(Design.Opacity.subtle), lineWidth: Design.LineWidth.thin)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
ScreenshotCameraPlaceholder(ringWidth: 70.0, ringColor: .white, ringOpacity: 0.7)
|
|
||||||
}
|
|
||||||
@ -203,7 +203,6 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.accessibilityIdentifier("settings-sheet")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Ring Size Slider
|
// MARK: - Ring Size Slider
|
||||||
|
|||||||
@ -997,7 +997,6 @@
|
|||||||
},
|
},
|
||||||
"Debug mode: Purchase simulated!" : {
|
"Debug mode: Purchase simulated!" : {
|
||||||
"comment" : "Announcement posted to VoiceOver when a premium purchase is simulated in debug mode.",
|
"comment" : "Announcement posted to VoiceOver when a premium purchase is simulated in debug mode.",
|
||||||
"extractionState" : "stale",
|
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"es-MX" : {
|
"es-MX" : {
|
||||||
@ -1022,7 +1021,6 @@
|
|||||||
},
|
},
|
||||||
"Debug mode: Restore simulated!" : {
|
"Debug mode: Restore simulated!" : {
|
||||||
"comment" : "Accessibility announcement when restoring purchases in debug mode.",
|
"comment" : "Accessibility announcement when restoring purchases in debug mode.",
|
||||||
"extractionState" : "stale",
|
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"es-MX" : {
|
"es-MX" : {
|
||||||
@ -1810,6 +1808,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Onboarding Reset" : {
|
||||||
|
"comment" : "The title of an alert that confirms the onboarding state has been reset.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Onboarding will show again when you restart the app." : {
|
||||||
|
"comment" : "A message displayed in an alert when the \"Reset Onboarding\" button is tapped.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"One Time" : {
|
"One Time" : {
|
||||||
"comment" : "A description of a one-time purchase option.",
|
"comment" : "A description of a one-time purchase option.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
@ -2127,6 +2133,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Reset Onboarding" : {
|
||||||
|
"comment" : "A button label that resets the onboarding state.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Restore Purchases" : {
|
"Restore Purchases" : {
|
||||||
"comment" : "A button that restores purchases.",
|
"comment" : "A button that restores purchases.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
@ -2777,6 +2787,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Show onboarding flow on next app launch" : {
|
||||||
|
"comment" : "A description of what the \"Reset Onboarding\" button does.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Shows a grid overlay to help compose your shot" : {
|
"Shows a grid overlay to help compose your shot" : {
|
||||||
"comment" : "A toggle that enables or disables the rule of thirds grid overlay in the camera view.",
|
"comment" : "A toggle that enables or disables the rule of thirds grid overlay in the camera view.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.9 MiB |
44
SelfieCam/Shared/Extensions/Color+Codable.swift
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
//
|
||||||
|
// Color+Codable.swift
|
||||||
|
// CameraTester
|
||||||
|
//
|
||||||
|
// Created by Matt Bruce on 1/3/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
// MARK: - Color Codable Extension
|
||||||
|
|
||||||
|
extension Color: Codable {
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case red, green, blue, opacity
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
let red = try container.decode(Double.self, forKey: .red)
|
||||||
|
let green = try container.decode(Double.self, forKey: .green)
|
||||||
|
let blue = try container.decode(Double.self, forKey: .blue)
|
||||||
|
let opacity = try container.decode(Double.self, forKey: .opacity)
|
||||||
|
|
||||||
|
self.init(red: red, green: green, blue: blue, opacity: opacity)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
|
||||||
|
// Convert Color to RGB components
|
||||||
|
let uiColor = UIColor(self)
|
||||||
|
var red: CGFloat = 0
|
||||||
|
var green: CGFloat = 0
|
||||||
|
var blue: CGFloat = 0
|
||||||
|
var alpha: CGFloat = 0
|
||||||
|
|
||||||
|
uiColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
|
||||||
|
|
||||||
|
try container.encode(Double(red), forKey: .red)
|
||||||
|
try container.encode(Double(green), forKey: .green)
|
||||||
|
try container.encode(Double(blue), forKey: .blue)
|
||||||
|
try container.encode(Double(alpha), forKey: .opacity)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,22 +11,17 @@ import SwiftUI
|
|||||||
struct CameraSettings: Codable {
|
struct CameraSettings: Codable {
|
||||||
var photoQuality: PhotoQuality
|
var photoQuality: PhotoQuality
|
||||||
var isRingLightEnabled: Bool
|
var isRingLightEnabled: Bool
|
||||||
var ringLightColorRGB: CustomColorRGB
|
var ringLightColor: Color
|
||||||
var ringLightSize: CGFloat
|
var ringLightSize: CGFloat
|
||||||
var ringLightOpacity: Double
|
var ringLightOpacity: Double
|
||||||
var flashMode: CameraFlashMode
|
var flashMode: CameraFlashMode
|
||||||
var isFlashSyncedWithRingLight: Bool
|
var isFlashSyncedWithRingLight: Bool
|
||||||
|
|
||||||
var ringLightColor: Color {
|
|
||||||
get { ringLightColorRGB.color }
|
|
||||||
set { ringLightColorRGB = CustomColorRGB(from: newValue) }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default settings
|
// Default settings
|
||||||
static let `default` = CameraSettings(
|
static let `default` = CameraSettings(
|
||||||
photoQuality: .high,
|
photoQuality: .high,
|
||||||
isRingLightEnabled: true,
|
isRingLightEnabled: true,
|
||||||
ringLightColorRGB: .defaultWhite,
|
ringLightColor: .white,
|
||||||
ringLightSize: 25,
|
ringLightSize: 25,
|
||||||
ringLightOpacity: 1.0,
|
ringLightOpacity: 1.0,
|
||||||
flashMode: .off,
|
flashMode: .off,
|
||||||
|
|||||||
@ -8,125 +8,34 @@
|
|||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
final class SelfieCamUITests: XCTestCase {
|
final class SelfieCamUITests: XCTestCase {
|
||||||
private var app: XCUIApplication!
|
|
||||||
|
|
||||||
override func setUpWithError() throws {
|
override func setUpWithError() throws {
|
||||||
|
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||||
|
|
||||||
|
// In UI tests it is usually best to stop immediately when a failure occurs.
|
||||||
continueAfterFailure = false
|
continueAfterFailure = false
|
||||||
app = XCUIApplication()
|
|
||||||
addUIInterruptionMonitor(withDescription: "System Permission Alerts") { alert in
|
|
||||||
let preferredButtons = [
|
|
||||||
"Allow",
|
|
||||||
"Allow While Using App",
|
|
||||||
"OK",
|
|
||||||
"Continue"
|
|
||||||
]
|
|
||||||
|
|
||||||
for title in preferredButtons where alert.buttons[title].exists {
|
// In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
|
||||||
alert.buttons[title].tap()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if let firstButton = alert.buttons.allElementsBoundByIndex.first {
|
|
||||||
firstButton.tap()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tearDownWithError() throws {
|
override func tearDownWithError() throws {
|
||||||
|
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func testAppStorePortraitScreenshots() throws {
|
func testExample() throws {
|
||||||
app.launchArguments = ["-ui-testing-screenshots"]
|
// UI tests must launch the application that they test.
|
||||||
app.launchEnvironment = [
|
let app = XCUIApplication()
|
||||||
"ENABLE_DEBUG_PREMIUM": "1"
|
|
||||||
]
|
|
||||||
app.launch()
|
app.launch()
|
||||||
if !waitForCameraScreen(timeout: 8) {
|
|
||||||
app.terminate()
|
|
||||||
app.launch()
|
|
||||||
}
|
|
||||||
|
|
||||||
saveScreenshot(named: "01-launch-screen")
|
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
||||||
XCTAssertTrue(waitForCameraScreen(timeout: 25), "Camera screen did not appear")
|
|
||||||
XCTAssertTrue(app.otherElements["screenshot-camera-placeholder"].waitForExistence(timeout: 5), "Screenshot camera placeholder did not appear")
|
|
||||||
RunLoop.current.run(until: Date().addingTimeInterval(0.6))
|
|
||||||
saveScreenshot(named: "02-camera-ring-light")
|
|
||||||
|
|
||||||
let settingsButton = app.buttons["Settings"]
|
|
||||||
XCTAssertTrue(settingsButton.waitForExistence(timeout: 10), "Settings button not found")
|
|
||||||
settingsButton.tap()
|
|
||||||
XCTAssertTrue(app.otherElements["settings-sheet"].waitForExistence(timeout: 8), "Settings sheet did not appear")
|
|
||||||
RunLoop.current.run(until: Date().addingTimeInterval(0.8))
|
|
||||||
saveScreenshot(named: "03-settings-popup")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func testLaunchPerformance() throws {
|
func testLaunchPerformance() throws {
|
||||||
|
// This measures how long it takes to launch your application.
|
||||||
measure(metrics: [XCTApplicationLaunchMetric()]) {
|
measure(metrics: [XCTApplicationLaunchMetric()]) {
|
||||||
XCUIApplication().launch()
|
XCUIApplication().launch()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Helpers
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
private func waitForCameraScreen(timeout: TimeInterval) -> Bool {
|
|
||||||
let onboardingActions = [
|
|
||||||
"Get Started",
|
|
||||||
"Continue",
|
|
||||||
"Enable Camera",
|
|
||||||
"Enable Microphone",
|
|
||||||
"Enable Photos",
|
|
||||||
"Done"
|
|
||||||
]
|
|
||||||
let deadline = Date().addingTimeInterval(timeout)
|
|
||||||
while Date() < deadline {
|
|
||||||
if app.otherElements["screenshot-mode-active"].exists {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
_ = tapFirstExistingButton(labels: onboardingActions)
|
|
||||||
tapFirstSpringboardButton(labels: ["Allow", "Allow While Using App", "OK", "Continue"])
|
|
||||||
if app.alerts.firstMatch.exists {
|
|
||||||
app.tap()
|
|
||||||
}
|
|
||||||
RunLoop.current.run(until: Date().addingTimeInterval(0.2))
|
|
||||||
}
|
|
||||||
return app.otherElements["screenshot-mode-active"].exists
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
private func tapFirstSpringboardButton(labels: [String]) {
|
|
||||||
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
|
|
||||||
for label in labels {
|
|
||||||
let button = springboard.buttons[label]
|
|
||||||
if button.waitForExistence(timeout: 0.2) {
|
|
||||||
button.tap()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
private func tapFirstExistingButton(labels: [String]) -> Bool {
|
|
||||||
for label in labels {
|
|
||||||
let button = app.buttons[label]
|
|
||||||
if button.exists {
|
|
||||||
button.tap()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
private func saveScreenshot(named name: String) {
|
|
||||||
let attachment = XCTAttachment(screenshot: app.screenshot())
|
|
||||||
attachment.name = name
|
|
||||||
attachment.lifetime = .keepAlways
|
|
||||||
add(attachment)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,13 @@ final class SelfieCamUITestsLaunchTests: XCTestCase {
|
|||||||
func testLaunch() throws {
|
func testLaunch() throws {
|
||||||
let app = XCUIApplication()
|
let app = XCUIApplication()
|
||||||
app.launch()
|
app.launch()
|
||||||
XCTAssertTrue(app.state == .runningForeground)
|
|
||||||
|
// Insert steps here to perform after app launch but before taking a screenshot,
|
||||||
|
// such as logging into a test account or navigating somewhere in the app
|
||||||
|
|
||||||
|
let attachment = XCTAttachment(screenshot: app.screenshot())
|
||||||
|
attachment.name = "Launch Screen"
|
||||||
|
attachment.lifetime = .keepAlways
|
||||||
|
add(attachment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||