├── Dangerfile ├── resources ├── result.png └── xcuitests.png ├── LeaksDetector ├── leaksdetector ├── .gitignore ├── Sources │ └── LeaksDetector │ │ ├── CommandSteps │ │ ├── RunCommandStep.swift │ │ ├── CleanUp.swift │ │ ├── PrepareStep.swift │ │ ├── Report.swift │ │ ├── SimulateUIFlow.swift │ │ ├── GenerateMemgraphFile.swift │ │ └── CheckLeaks.swift │ │ ├── Helpers │ │ ├── String+.swift │ │ ├── Constants.swift │ │ └── Helpers.swift │ │ ├── LeaksDetector.swift │ │ ├── Executor │ │ ├── MaestroExecutor.swift │ │ ├── Executor.swift │ │ └── XCUITestExecutor.swift │ │ └── Commands │ │ ├── MaestroCommand.swift │ │ ├── Processor.swift │ │ └── XCUITestCommand.swift ├── Tests │ └── LeaksDetectorTests │ │ └── ProcessorTests.swift ├── Package.resolved ├── Package.swift ├── README.md └── .swiftpm │ └── xcode │ └── xcshareddata │ └── xcschemes │ └── LeaksDetector.xcscheme ├── Gemfile ├── MemoryLeaksCheck ├── Assets.xcassets │ ├── Contents.json │ ├── AccentColor.colorset │ │ └── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── Info.plist ├── AppDelegate.swift ├── Scenarios │ ├── AbandonedMemoryExampleVC.swift │ └── LeaksExampleVC.swift ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard └── ViewController.swift ├── MemoryLeaksCheck.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcuserdata │ │ ├── tuanhoang.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ │ └── hoanganhtuan.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved ├── xcshareddata │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ ├── xcbaselines │ │ └── 4CBC59372AC3299B00D83CA5.xcbaseline │ │ │ ├── A467896A-0CFB-4361-8284-254E52BCD3B2.plist │ │ │ └── Info.plist │ └── xcschemes │ │ ├── LeaksCheckerUITests.xcscheme │ │ └── MemoryLeaksCheck.xcscheme ├── xcuserdata │ ├── hoanganhtuan.xcuserdatad │ │ ├── xcdebugger │ │ │ └── Breakpoints_v2.xcbkptlist │ │ └── xcschemes │ │ │ └── xcschememanagement.plist │ └── tuanhoang.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist └── project.pbxproj ├── .github ├── ISSUE_TEMPLATE │ ├── improve.md │ ├── feature-request.md │ └── bug-report.md ├── pull_request_template.md └── workflows │ └── check-leaks.yml ├── maestro └── leaksCheckFlow.yaml ├── Dangerfile.leaksReport ├── Docs ├── Subcommand.md ├── Report.md └── XCUITests.md ├── LeaksCheckerUITests └── LeaksCheckerUITests.swift ├── Gemfile.lock └── README.md /Dangerfile: -------------------------------------------------------------------------------- 1 | essage("Hello") -------------------------------------------------------------------------------- /resources/result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangatuan/MemoryLeaksCheck/HEAD/resources/result.png -------------------------------------------------------------------------------- /resources/xcuitests.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangatuan/MemoryLeaksCheck/HEAD/resources/xcuitests.png -------------------------------------------------------------------------------- /LeaksDetector/leaksdetector: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangatuan/MemoryLeaksCheck/HEAD/LeaksDetector/leaksdetector -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # gem "rails" 6 | 7 | gem 'danger' -------------------------------------------------------------------------------- /MemoryLeaksCheck/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /MemoryLeaksCheck/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LeaksDetector/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /MemoryLeaksCheck.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MemoryLeaksCheck.xcodeproj/xcshareddata/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /MemoryLeaksCheck/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /MemoryLeaksCheck.xcodeproj/xcuserdata/hoanganhtuan.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /LeaksDetector/Sources/LeaksDetector/CommandSteps/RunCommandStep.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Tuan Hoang on 6/1/24. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol RunCommandStep { 11 | func run() throws 12 | } 13 | -------------------------------------------------------------------------------- /MemoryLeaksCheck.xcodeproj/project.xcworkspace/xcuserdata/tuanhoang.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangatuan/MemoryLeaksCheck/HEAD/MemoryLeaksCheck.xcodeproj/project.xcworkspace/xcuserdata/tuanhoang.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /MemoryLeaksCheck.xcodeproj/project.xcworkspace/xcuserdata/hoanganhtuan.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangatuan/MemoryLeaksCheck/HEAD/MemoryLeaksCheck.xcodeproj/project.xcworkspace/xcuserdata/hoanganhtuan.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /MemoryLeaksCheck/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/improve.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Improve a Feature 3 | about: Create a report to help us improve an existing feature 4 | title: IMPROVE - 5 | labels: improvement 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Describe the feature you want to improve 11 | 12 | 13 | ## Describe alternatives you've considered 14 | -------------------------------------------------------------------------------- /MemoryLeaksCheck.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LeaksDetector/Sources/LeaksDetector/Helpers/String+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Tuan Hoang on 6/1/24. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String { 11 | func matches(_ regex: String) -> Bool { 12 | return self.range(of: regex, options: .regularExpression, range: nil, locale: nil) != nil 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /maestro/leaksCheckFlow.yaml: -------------------------------------------------------------------------------- 1 | appId: Hoang-Anh-Tuan.MemoryLeaksCheck 2 | --- 3 | - launchApp 4 | - tapOn: "Abandoned Memory Example" 5 | - tapOn: "Scenarios" 6 | - tapOn: "Leaks Memory Example" 7 | - tapOn: "Simulate Logout then Login Action" 8 | - tapOn: "Simulate Logout then Login Action" 9 | - tapOn: "Simulate Logout then Login Action" 10 | - tapOn: "Simulate Logout then Login Action" -------------------------------------------------------------------------------- /MemoryLeaksCheck.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "snapkit", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/SnapKit/SnapKit", 7 | "state" : { 8 | "branch" : "develop", 9 | "revision" : "58320fe80522414bf3a7e24c88123581dc586752" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /LeaksDetector/Sources/LeaksDetector/CommandSteps/CleanUp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Tuan Hoang on 6/1/24. 6 | // 7 | 8 | import Foundation 9 | import ShellOut 10 | 11 | struct CleanUp: RunCommandStep { 12 | let executor: Executor 13 | 14 | func run() throws { 15 | log(message: "Cleaning... 🧹") 16 | _ = try? shellOut(to: "rm \(Constants.leaksReportFileName)") 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /LeaksDetector/Sources/LeaksDetector/CommandSteps/PrepareStep.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Tuan Hoang Anh on 18/8/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct PrepareStep: RunCommandStep { 11 | 12 | func run() throws { 13 | try? FileManager.default.removeItem(at: Constants.memgraphsFolder) 14 | try FileManager.default.createDirectory(at: Constants.memgraphsFolder, withIntermediateDirectories: true) 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /Dangerfile.leaksReport: -------------------------------------------------------------------------------- 1 | # Function to read a message from a file 2 | def read_message_from_file(file_path) 3 | if File.exist?(file_path) 4 | File.read(file_path).strip 5 | else 6 | nil 7 | end 8 | end 9 | 10 | # Define the path to the file containing the message 11 | message_file = "./temporary.txt" 12 | 13 | # Read the message from the file 14 | message = read_message_from_file(message_file) 15 | 16 | # Check if the message is not empty 17 | if message && !message.empty? 18 | fail("Leaks detected:\n #{message}") 19 | else 20 | nil 21 | end -------------------------------------------------------------------------------- /Docs/Subcommand.md: -------------------------------------------------------------------------------- 1 | 2 | # How to use specific subcommand 3 | 4 | 1. Maestro 5 | 6 | ## Maestro 7 | 8 | To use Maestro, you need to follow these steps: 9 | 10 | 1. Install Maestro. Learn more about `Maestro` [here](https://maestro.mobile.dev/) 11 | 2. Create a maestro flow to run simulate the flow in your app. 12 | 3. In your ci workflow, just call: 13 | ```bash 14 | leaksdetector maestro -p $YOUR_APP_NAME -maestro-flow-path $YOUR_MAESTRO_FILE_PATH 15 | ``` 16 | 17 | 4. If you want to report the result to your Pull request/Slack/..., please check out the Report section. 18 | -------------------------------------------------------------------------------- /LeaksDetector/Sources/LeaksDetector/LeaksDetector.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | import ArgumentParser 4 | import ShellOut 5 | import Darwin 6 | 7 | @main 8 | struct LeaksDetector: ParsableCommand { 9 | static let configuration = CommandConfiguration( 10 | commandName: "leaksdetector", 11 | abstract: "This program wraps up the logic integrate leaks checking with your CI workflow", 12 | subcommands: [ 13 | MaestroCommand.self, 14 | XCUITestCommand.self 15 | ], 16 | defaultSubcommand: MaestroCommand.self 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for this project 4 | title: FEATURE - 5 | labels: feature request 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Describe the feature you want the app to have that it currently doesn't 11 | * A clear and concise description of what you want to happen. 12 | 13 | ## Describe alternatives you've considered 14 | * A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | ## Additional context 17 | * Add any other context or screenshots about the feature request here. -------------------------------------------------------------------------------- /LeaksDetector/Sources/LeaksDetector/Helpers/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Tuan Hoang on 6/1/24. 6 | // 7 | 8 | import Foundation 9 | 10 | enum Constants { 11 | static let leaksReportFileName = "leaksReport.txt" 12 | static var memgraphsFolder: URL { 13 | let currentDirectory = FileManager.default.currentDirectoryPath 14 | let memgraphsFolder = URL(fileURLWithPath: currentDirectory).appendingPathComponent("memgraphs") 15 | print("Memgraphs folder: \(memgraphsFolder.path)") 16 | return memgraphsFolder 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /LeaksDetector/Sources/LeaksDetector/CommandSteps/Report.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Tuan Hoang on 6/1/24. 6 | // 7 | 8 | import Foundation 9 | import ShellOut 10 | 11 | struct DangerReportStep: RunCommandStep { 12 | let dangerFilePath: String 13 | 14 | func run() throws { 15 | do { 16 | try shellOut(to: "bundle exec danger --dangerfile=\(dangerFilePath) --danger_id=LeaksReport") 17 | } catch let error { 18 | log(message: "❌ Can not execute Danger", color: .red) 19 | throw error 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /LeaksDetector/Sources/LeaksDetector/Executor/MaestroExecutor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Tuan Hoang Anh on 18/8/24. 6 | // 7 | 8 | import Foundation 9 | import ShellOut 10 | 11 | final class MaestroExecutor: DefaultExecutor { 12 | 13 | /// Maestro needs a flow.yaml to start simulating UI 14 | private let flowPath: String 15 | 16 | init(flowPath: String) { 17 | self.flowPath = flowPath 18 | super.init() 19 | } 20 | 21 | override func simulateUI() throws { 22 | try shellOut(to: "maestro test \(flowPath)") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Docs/Report.md: -------------------------------------------------------------------------------- 1 | 2 | # Supported report process 3 | 4 | 1. Danger ✅ 5 | 2. Slack (In progress) 6 | 7 | ## Danger 8 | 9 | 1. Create a new Dangerfile to handle the process checking for leaks. 10 | You can copy `Dangerfile.leaksReport` in the example project. 11 | 12 | 2. You can customize the Dangerfile to do your own things. 13 | 14 | ### Why do I need to create a separated Dangerfile? 15 | 16 | Because the process checking for leaks will trigger Danger to run. 17 | If you combine the leaks checking Dangerfile and your project Dangerfile to 1 Dangerfile only, the leaks process will execute your Danger logic as well. 18 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # What it Does 2 | * Closes #issueNumber 3 | * Describe what your change does 4 | 5 | # How I Tested 6 | * Add a list of steps to show the functionality of your feature 7 | For example: 8 | * Run the application 9 | * Tap the (+) button 10 | * See this screen 11 | etc... 12 | 13 | # Notes 14 | * Anything else that should be noted about how you implemented this feature? 15 | 16 | # Screenshot 17 | * Add a screenshot of your new feature! OR show a screen recording of it in action. (On the simulator press Cmd + R to record, and the stop button when done). **This video should be no longer than 30 seconds.** -------------------------------------------------------------------------------- /LeaksDetector/Sources/LeaksDetector/CommandSteps/SimulateUIFlow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Tuan Hoang on 6/1/24. 6 | // 7 | 8 | import ShellOut 9 | import Foundation 10 | 11 | struct SimulateUIFlow: RunCommandStep { 12 | let executor: Executor 13 | 14 | func run() throws { 15 | log(message: "Start running ui flow... 🎥") 16 | do { 17 | try executor.simulateUI() 18 | } catch let error { 19 | let error = error as! ShellOutError 20 | log(message: "❌ Something went wrong when trying to capture ui flow. \(error.message)", color: .red) 21 | throw error 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LeaksDetector/Sources/LeaksDetector/CommandSteps/GenerateMemgraphFile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Tuan Hoang on 6/1/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct GenerateMemgraphFile: RunCommandStep { 11 | let executor: Executor 12 | let processName: String 13 | 14 | func run() throws { 15 | do { 16 | try executor.generateMemgraph(for: processName) 17 | log(message: "Generate memgraph successfully for process 🚀", color: .green) 18 | } catch let error { 19 | log(message: "❌ Can not find any process with name: \(processName)", color: .red) 20 | throw error 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /LeaksDetector/Sources/LeaksDetector/Helpers/Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Hoang Anh Tuan on 30/09/2023. 6 | // 7 | 8 | import Foundation 9 | import TSCBasic 10 | import TSCUtility 11 | 12 | let terminalController = TerminalController(stream: stdoutStream) 13 | 14 | func log( 15 | message: @autoclosure () -> String, 16 | color: TerminalController.Color = .noColor, 17 | needEndline: Bool = true, 18 | isBold: Bool = false 19 | ) { 20 | #if DEBUG 21 | debugPrint(message()) 22 | #else 23 | terminalController?.write(message(), inColor: color, bold: isBold) 24 | 25 | if needEndline { 26 | terminalController?.endLine() 27 | } 28 | #endif 29 | } 30 | -------------------------------------------------------------------------------- /MemoryLeaksCheck/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // MemoryLeaksCheck 4 | // 5 | // Created by Hoang Anh Tuan on 23/09/2023. 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 | 17 | window = UIWindow(frame: UIScreen.main.bounds) 18 | window?.rootViewController = UINavigationController(rootViewController: ViewController()) 19 | window?.makeKeyAndVisible() 20 | 21 | return true 22 | } 23 | 24 | } 25 | 26 | -------------------------------------------------------------------------------- /MemoryLeaksCheck/Scenarios/AbandonedMemoryExampleVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AbandonedMemoryLeaksVC.swift 3 | // MemoryLeaksCheck 4 | // 5 | // Created by Hoang Anh Tuan on 23/09/2023. 6 | // 7 | 8 | import UIKit 9 | 10 | class Person { 11 | var apartment: Apartment? 12 | } 13 | 14 | class Apartment { 15 | var person: Person? 16 | } 17 | 18 | final class AbandonedMemoryVC: UIViewController { 19 | 20 | let apartment = Apartment() 21 | let person = Person() 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | 26 | title = "Abandoned Memory Example" 27 | view.backgroundColor = .white 28 | 29 | apartment.person = person 30 | person.apartment = apartment 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report about a feature that isn't working properly 4 | title: BUG - 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Describe the bug 11 | * A clear and concise description of what the bug is. 12 | 13 | ## To Reproduce 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Tap on this button '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | ## Expected behavior 21 | * A clear and concise description of what you expected to happen. 22 | 23 | ## Screenshots 24 | * If applicable, add screenshots to help explain your problem. 25 | 26 | ## Device Info (please complete the following information): 27 | * Device: [e.g. iPhone14] 28 | * OS: [e.g. iOS 16.0] 29 | * App Version [e.g. v1.5] 30 | 31 | ## Additional Context 32 | * Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /MemoryLeaksCheck.xcodeproj/xcshareddata/xcbaselines/4CBC59372AC3299B00D83CA5.xcbaseline/A467896A-0CFB-4361-8284-254E52BCD3B2.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | classNames 6 | 7 | LeaksCheckerUITests 8 | 9 | testExample() 10 | 11 | com.apple.dt.XCTMetric_Memory-Hoang-Anh-Tuan.MemoryLeaksCheck.physical 12 | 13 | baselineAverage 14 | 0.000000 15 | baselineIntegrationDisplayName 16 | Local Baseline 17 | 18 | com.apple.dt.XCTMetric_Memory-Hoang-Anh-Tuan.MemoryLeaksCheck.physical_peak 19 | 20 | baselineAverage 21 | 0.000000 22 | baselineIntegrationDisplayName 23 | Local Baseline 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /MemoryLeaksCheck.xcodeproj/xcuserdata/tuanhoang.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | LeaksCheckerUITests.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | MemoryLeaksCheck.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 1 16 | 17 | SnapKitPlayground (Playground) 1.xcscheme 18 | 19 | isShown 20 | 21 | orderHint 22 | 3 23 | 24 | SnapKitPlayground (Playground) 2.xcscheme 25 | 26 | isShown 27 | 28 | orderHint 29 | 4 30 | 31 | SnapKitPlayground (Playground).xcscheme 32 | 33 | isShown 34 | 35 | orderHint 36 | 2 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /MemoryLeaksCheck.xcodeproj/xcuserdata/hoanganhtuan.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | MemoryLeaksCheck.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | SnapKitPlayground (Playground) 1.xcscheme 13 | 14 | isShown 15 | 16 | orderHint 17 | 2 18 | 19 | SnapKitPlayground (Playground) 2.xcscheme 20 | 21 | isShown 22 | 23 | orderHint 24 | 3 25 | 26 | SnapKitPlayground (Playground).xcscheme 27 | 28 | isShown 29 | 30 | orderHint 31 | 1 32 | 33 | 34 | SuppressBuildableAutocreation 35 | 36 | 4C28E3C92ABEDFFB0004394E 37 | 38 | primary 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /MemoryLeaksCheck.xcodeproj/xcshareddata/xcbaselines/4CBC59372AC3299B00D83CA5.xcbaseline/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | runDestinationsByUUID 6 | 7 | A467896A-0CFB-4361-8284-254E52BCD3B2 8 | 9 | localComputer 10 | 11 | busSpeedInMHz 12 | 100 13 | cpuCount 14 | 1 15 | cpuKind 16 | Dual-Core Intel Core i5 17 | cpuSpeedInMHz 18 | 2300 19 | logicalCPUCoresPerPackage 20 | 4 21 | modelCode 22 | MacBookPro14,1 23 | physicalCPUCoresPerPackage 24 | 2 25 | platformIdentifier 26 | com.apple.platform.macosx 27 | 28 | targetArchitecture 29 | x86_64 30 | targetDevice 31 | 32 | modelCode 33 | iPhone15,2 34 | platformIdentifier 35 | com.apple.platform.iphonesimulator 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /LeaksCheckerUITests/LeaksCheckerUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LeaksCheckerUITests.swift 3 | // LeaksCheckerUITests 4 | // 5 | // Created by Hoang Anh Tuan on 26/09/2023. 6 | // 7 | 8 | import Foundation 9 | import XCTest 10 | 11 | final class LeaksCheckerUITests: XCTestCase { 12 | 13 | var app: XCUIApplication! 14 | 15 | override func tearDownWithError() throws { 16 | app = nil 17 | super.tearDown() 18 | } 19 | 20 | /// This test will generate memgraph via command line. However, it only works on physical device, not simulator. 21 | /// For more info, please read README. 22 | func testMemoryLeaks1() throws { 23 | let app = XCUIApplication() 24 | let options = XCTMeasureOptions() 25 | options.iterationCount = 1 26 | 27 | measure( 28 | metrics: [XCTMemoryMetric(application: app)], 29 | options: options 30 | ) { 31 | app.launch() 32 | startMeasuring() 33 | 34 | app.staticTexts["Leaks Memory Example"].tap() 35 | 36 | let simulateLogoutThenLoginActionButton = app.buttons["Simulate Logout then Login Action"] 37 | simulateLogoutThenLoginActionButton.tap() 38 | simulateLogoutThenLoginActionButton.tap() 39 | simulateLogoutThenLoginActionButton.tap() 40 | simulateLogoutThenLoginActionButton.tap() 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /LeaksDetector/Tests/LeaksDetectorTests/ProcessorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProcessorTests.swift 3 | // 4 | // 5 | // Created by Tuan Hoang on 6/1/24. 6 | // 7 | 8 | //import XCTest 9 | //@testable import LeaksDetector 10 | // 11 | //final class ProcessorTests: XCTestCase { 12 | // 13 | // func testGenerateStepsCorrectly() { 14 | // let sut = Processor( 15 | // configuration: .init(processName: "", dangerFilePath: "A"), 16 | // executor: MaestroExecutor(flowPath: "abc") 17 | // ) 18 | // 19 | // XCTAssertTypeEqual(sut.steps[0], PrepareStep.self) 20 | // XCTAssertTypeEqual(sut.steps[1], SimulateUIFlow.self) 21 | // XCTAssertTypeEqual(sut.steps[2], GenerateMemgraphFile.self) 22 | // XCTAssertTypeEqual(sut.steps[3], CheckLeaks.self) 23 | // XCTAssertTypeEqual(sut.steps[4], DangerReportStep.self) 24 | // XCTAssertTypeEqual(sut.steps[5], CleanUp.self) 25 | // 26 | // XCTAssertEqual(sut.steps.count, 6) 27 | // } 28 | //} 29 | // 30 | //public func XCTAssertTypeEqual( 31 | // _ lhs: Any?, 32 | // _ rhs: (some Any).Type, 33 | // file: StaticString = #filePath, 34 | // line: UInt = #line 35 | //) { 36 | // guard let lhs else { 37 | // return XCTFail("First argument should not be nil", file: file, line: line) 38 | // } 39 | // 40 | // if type(of: lhs) != rhs { 41 | // XCTFail("Expected \(rhs), got \(type(of: lhs))", file: file, line: line) 42 | // } 43 | //} 44 | -------------------------------------------------------------------------------- /LeaksDetector/Sources/LeaksDetector/Commands/MaestroCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Tuan Hoang on 6/1/24. 6 | // 7 | 8 | import ArgumentParser 9 | import Foundation 10 | 11 | struct MaestroCommand: ParsableCommand { 12 | static let configuration = CommandConfiguration( 13 | commandName: "maestro", 14 | abstract: "Perform memory leaks check by using Maestro testing tool" 15 | ) 16 | 17 | #if DEBUG 18 | private var processName = "MemoryLeaksCheck" 19 | private var maestroFlowPath: String = "/Users/hoanganhtuan/Desktop/MemoryLeaksCheck/maestro/leaksCheckFlow.yaml" 20 | private var dangerFilePath: String = "Dangerfile.leaksReport" 21 | #else 22 | @Option(name: .shortAndLong, help: "The name of the running process") 23 | private var processName: String 24 | 25 | @Option(name: .long, help: "The path to the maestro ui testing yaml file") 26 | private var maestroFlowPath: String 27 | 28 | @Option(name: .long, help: "The path to the Dangerfile") 29 | private var dangerFilePath: String? 30 | #endif 31 | 32 | public func run() throws { 33 | let executor = MaestroExecutor(flowPath: maestroFlowPath) 34 | let processor = Processor( 35 | configuration: .init( 36 | processName: processName, 37 | dangerFilePath: dangerFilePath 38 | ), 39 | executor: executor 40 | ) 41 | 42 | processor.start() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /LeaksDetector/Sources/LeaksDetector/Executor/Executor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Hoang Anh Tuan on 30/09/2023. 6 | // 7 | 8 | import Foundation 9 | import ShellOut 10 | import ArgumentParser 11 | 12 | protocol Executor { 13 | func simulateUI() throws 14 | func generateMemgraph(for processName: String) throws 15 | func memgraphPaths() -> [String] 16 | } 17 | 18 | class DefaultExecutor: Executor { 19 | init() { } 20 | 21 | func simulateUI() throws { 22 | fatalError("Need to override this func") 23 | } 24 | 25 | func generateMemgraph(for processName: String) throws { 26 | let memgraphPath = Constants.memgraphsFolder.path() + "/\(UUID().uuidString).memgraph" 27 | try shellOut(to: "leaks \(processName) --outputGraph=\(memgraphPath)") 28 | } 29 | 30 | func memgraphPaths() -> [String] { 31 | let fileManager = FileManager.default 32 | var memgraphFiles: [URL] = [] 33 | 34 | // Create a directory enumerator to recursively go through the folder 35 | if let enumerator = fileManager.enumerator(at: Constants.memgraphsFolder, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]) { 36 | // Iterate through the files and directories 37 | for case let fileURL as URL in enumerator { 38 | if fileURL.pathExtension == "memgraph" { 39 | memgraphFiles.append(fileURL) 40 | } 41 | } 42 | } 43 | 44 | return memgraphFiles.map { $0.path() } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /LeaksDetector/Sources/LeaksDetector/Commands/Processor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Tuan Hoang on 6/1/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Configuration { 11 | let processName: String 12 | let dangerFilePath: String? 13 | } 14 | 15 | struct Processor { 16 | let configuration: Configuration 17 | let executor: Executor 18 | let steps: [RunCommandStep] 19 | 20 | init(configuration: Configuration, executor: Executor) { 21 | self.configuration = configuration 22 | self.executor = executor 23 | var steps: [RunCommandStep] = [ 24 | PrepareStep(), 25 | SimulateUIFlow(executor: executor), 26 | GenerateMemgraphFile(executor: executor, processName: configuration.processName), 27 | CheckLeaks(executor: executor) 28 | ] 29 | 30 | if let dangerFilePath = configuration.dangerFilePath { 31 | steps.append(DangerReportStep(dangerFilePath: dangerFilePath)) 32 | } 33 | 34 | steps.append(CleanUp(executor: executor)) 35 | 36 | self.steps = steps 37 | } 38 | 39 | private var processName: String { 40 | configuration.processName 41 | } 42 | 43 | func start() { 44 | do { 45 | try steps.forEach { step in 46 | try step.run() 47 | } 48 | log(message: "✅ The process finish successfully", color: .green) 49 | } catch { 50 | log(message: "❌ End the process with error", color: .red) 51 | Darwin.exit(EXIT_FAILURE) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /LeaksDetector/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "shellout", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/JohnSundell/ShellOut.git", 7 | "state" : { 8 | "revision" : "e1577acf2b6e90086d01a6d5e2b8efdaae033568", 9 | "version" : "2.3.0" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-argument-parser", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/apple/swift-argument-parser", 16 | "state" : { 17 | "revision" : "8f4d2753f0e4778c76d5f05ad16c74f707390531", 18 | "version" : "1.2.3" 19 | } 20 | }, 21 | { 22 | "identity" : "swift-system", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/apple/swift-system.git", 25 | "state" : { 26 | "revision" : "025bcb1165deab2e20d4eaba79967ce73013f496", 27 | "version" : "1.2.1" 28 | } 29 | }, 30 | { 31 | "identity" : "swift-tools-support-core", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/apple/swift-tools-support-core.git", 34 | "state" : { 35 | "revision" : "3b13e439a341bbbfe0f710c7d1be37221745ef1a", 36 | "version" : "0.6.1" 37 | } 38 | }, 39 | { 40 | "identity" : "xcresultkit", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/davidahouse/XCResultKit.git", 43 | "state" : { 44 | "revision" : "63a93e454e30084b8948cba0ee42112fca1f6010", 45 | "version" : "1.2.0" 46 | } 47 | } 48 | ], 49 | "version" : 2 50 | } 51 | -------------------------------------------------------------------------------- /LeaksDetector/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "LeaksDetector", 8 | platforms: [ 9 | .macOS(.v13), 10 | .iOS(.v15) 11 | ], 12 | products: [ 13 | .executable(name: "leaksdetector", targets: ["LeaksDetector"]) 14 | ], 15 | dependencies: [ 16 | .package(url: "https://github.com/apple/swift-argument-parser", from: "1.2.0"), 17 | .package(url: "https://github.com/JohnSundell/ShellOut.git", from: "2.0.0"), 18 | .package(url: "https://github.com/apple/swift-tools-support-core", from: "0.6.0"), 19 | .package(url: "https://github.com/davidahouse/XCResultKit.git", exact: "1.2.0"), 20 | ], 21 | targets: [ 22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 23 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 24 | .executableTarget( 25 | name: "LeaksDetector", 26 | dependencies: [ 27 | .product(name: "ArgumentParser", package: "swift-argument-parser"), 28 | "ShellOut", 29 | .product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"), 30 | .product(name: "XCResultKit", package: "XCResultKit"), 31 | ] 32 | ), 33 | .testTarget( 34 | name: "LeaksDetectorTests", 35 | dependencies: ["LeaksDetector"] 36 | ), 37 | ] 38 | ) 39 | -------------------------------------------------------------------------------- /LeaksDetector/README.md: -------------------------------------------------------------------------------- 1 | # LeaksDetector 2 | 3 | A executable program to checks for leaks in a running program 4 | 5 | ## How it works? 6 | 7 | 1. It run `maestro` to capture memory graph. Find out about Maestro here: https://maestro.mobile.dev/ 8 | 2. It use `leaks` tool by Apple to generate a `memgraph` file, which contains all the leaks info if any. 9 | 3. Check for leaks 10 | 4. If there are any leaks, use `Danger` to post a message & mark a PR as failed. 11 | 12 | ## How to execute? 13 | 14 | Just need to run the script: 15 | 16 | ```bash 17 | $ leaksdetector maestro -p $PROGRAM_NAME -maestroFlowPath $MAESTRO_PATH_FILE -dangerFilePath $DANGER_FILE_PATH 18 | ``` 19 | 20 | ## How to run local 21 | 22 | 1. Open `MaestroCommand.swift`, change `processName`, `maestroFlowPath`, `dangerPath` to a hardcoded value 23 | 2. Run: 24 | 25 | ```bash 26 | $ swift run 27 | ``` 28 | 29 | ## How to release new executable file? 30 | 31 | ```bash 32 | $ swift build -c release 33 | $ cd .build/release 34 | $ cp -f leaksdetector ./../../../ 35 | ``` 36 | 37 | ### References: 38 | 39 | - https://www.fivestars.blog/articles/ultimate-guide-swift-executables/ 40 | - https://www.fivestars.blog/articles/a-look-into-argument-parser/ 41 | - https://www.swiftbysundell.com/articles/building-a-command-line-tool-using-the-swift-package-manager/ 42 | - https://www.avanderlee.com/swift/command-line-tool-package-manager/ 43 | - Enum arguments: https://swiftinit.org/docs/swift-argument-parser/argumentparser/customizingcompletions 44 | - Split to multiple Danger instance: https://www.jessesquires.com/blog/2020/12/15/running-multiple-dangers/ 45 | - Passing params to Danger: https://github.com/danger/swift/issues/213 46 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.8.5) 5 | public_suffix (>= 2.0.2, < 6.0) 6 | base64 (0.1.1) 7 | claide (1.1.0) 8 | claide-plugins (0.9.2) 9 | cork 10 | nap 11 | open4 (~> 1.3) 12 | colored2 (3.1.2) 13 | cork (0.3.0) 14 | colored2 (~> 3.1) 15 | danger (9.3.2) 16 | claide (~> 1.0) 17 | claide-plugins (>= 0.9.2) 18 | colored2 (~> 3.1) 19 | cork (~> 0.1) 20 | faraday (>= 0.9.0, < 3.0) 21 | faraday-http-cache (~> 2.0) 22 | git (~> 1.13) 23 | kramdown (~> 2.3) 24 | kramdown-parser-gfm (~> 1.0) 25 | no_proxy_fix 26 | octokit (~> 6.0) 27 | terminal-table (>= 1, < 4) 28 | faraday (2.7.11) 29 | base64 30 | faraday-net_http (>= 2.0, < 3.1) 31 | ruby2_keywords (>= 0.0.4) 32 | faraday-http-cache (2.5.0) 33 | faraday (>= 0.8) 34 | faraday-net_http (3.0.2) 35 | git (1.18.0) 36 | addressable (~> 2.8) 37 | rchardet (~> 1.8) 38 | kramdown (2.4.0) 39 | rexml 40 | kramdown-parser-gfm (1.1.0) 41 | kramdown (~> 2.0) 42 | nap (1.1.0) 43 | no_proxy_fix (0.1.2) 44 | octokit (6.1.1) 45 | faraday (>= 1, < 3) 46 | sawyer (~> 0.9) 47 | open4 (1.3.4) 48 | public_suffix (5.0.3) 49 | rchardet (1.8.0) 50 | rexml (3.2.6) 51 | ruby2_keywords (0.0.5) 52 | sawyer (0.9.2) 53 | addressable (>= 2.3.5) 54 | faraday (>= 0.17.3, < 3) 55 | terminal-table (3.0.2) 56 | unicode-display_width (>= 1.1.1, < 3) 57 | unicode-display_width (2.4.2) 58 | 59 | PLATFORMS 60 | ruby 61 | 62 | DEPENDENCIES 63 | danger 64 | 65 | BUNDLED WITH 66 | 2.1.4 67 | -------------------------------------------------------------------------------- /MemoryLeaksCheck/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /.github/workflows/check-leaks.yml: -------------------------------------------------------------------------------- 1 | # How to setup danger with github actions: https://www.jessesquires.com/blog/2020/04/10/running-danger-on-github-actions/ 2 | 3 | name: Check leaks 4 | run-name: ${{ github.actor }} is checking for leaks 🚀 5 | on: [pull_request] 6 | jobs: 7 | Explore-Github-Actions: 8 | runs-on: self-hosted 9 | steps: 10 | - run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event." 11 | - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!" 12 | - run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}." 13 | 14 | - name: Check out repository code 15 | uses: actions/checkout@v4 16 | 17 | # - uses: maxim-lobanov/setup-xcode@v1.6.0 18 | # with: 19 | # xcode-version: 14.2.0 20 | 21 | - name: ruby versions 22 | run: | 23 | ruby --version 24 | gem --version 25 | bundler --version 26 | 27 | # Should help this step to install dependencies. Since this is example project, I will skip this step. 28 | # - name: ruby setup 29 | # uses: ruby/setup-ruby@v1 30 | # with: 31 | # ruby-version: 2.7 32 | # bundler-cache: true 33 | 34 | # - run: xcodebuild -project MemoryLeaksCheck.xcodeproj -scheme MemoryLeaksCheck -destination 'platform=iOS Simulator,name=iPhone 14 Pro' CONFIGURATION_BUILD_DIR=$PWD/build 35 | 36 | - name: Check for leaks 37 | env: 38 | DANGER_GITHUB_API_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }} 39 | run: ./LeaksDetector/leaksdetector --process-name MemoryLeaksCheck --executor-type maestro --maestro-flow-path ./maestro/leaksCheckFlow.yaml -d ./Dangerfile.leaksReport 40 | 41 | - run: echo "🍏 This job's status is ${{ job.status }}." -------------------------------------------------------------------------------- /LeaksDetector/Sources/LeaksDetector/Commands/XCUITestCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Tuan Hoang Anh on 18/8/24. 6 | // 7 | 8 | import ArgumentParser 9 | import Foundation 10 | 11 | struct XCUITestCommand: ParsableCommand { 12 | 13 | static let configuration = CommandConfiguration( 14 | commandName: "xcuitest", 15 | abstract: "Perform memory leaks check by using XCUITest" 16 | ) 17 | 18 | #if DEBUG 19 | private var project: String? = "/Users/tuanhoang/Documents/MemoryLeaksCheck/MemoryLeaksCheck.xcodeproj" 20 | private var workspace: String? = nil 21 | private var scheme: String = "MemoryLeaksCheck" 22 | private var destination: String = "'platform=iOS,name=Tuan iPhone'" 23 | #else 24 | @Option(name: .shortAndLong, help: "build the project NAME") 25 | private var project: String? 26 | 27 | @Option(name: .shortAndLong, help: "build the workspace NAME") 28 | private var workspace: String 29 | 30 | @Option(name: .long, help: "build the scheme NAME") 31 | private var scheme: String 32 | 33 | @Option(name: .long, help: "use the destination described by DESTINATIONSPECIFIER (a comma-separated set of key=value pairs describing the destination to use. Please note that the destination must be real iOS device") 34 | private var destination: String 35 | #endif 36 | 37 | public func run() throws { 38 | let executor = try XCUITestExecutor( 39 | project: project, 40 | workspace: workspace, 41 | scheme: scheme, 42 | destination: destination 43 | ) 44 | 45 | let processor = Processor( 46 | configuration: .init( 47 | processName: "", 48 | dangerFilePath: nil 49 | ), 50 | executor: executor 51 | ) 52 | 53 | processor.start() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /MemoryLeaksCheck/Scenarios/LeaksExampleVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LeaksExample.swift 3 | // MemoryLeaksCheck 4 | // 5 | // Created by Hoang Anh Tuan on 23/09/2023. 6 | // 7 | 8 | import UIKit 9 | 10 | final class HomeViewController: UIViewController { 11 | 12 | let viewModel = HomeViewModel() 13 | 14 | private let randomNumberLabel = UILabel() 15 | private let changeRootVCButton: UIButton = { 16 | let button = UIButton(type: .system) 17 | button.setTitle("Simulate Logout then Login Action", for: .normal) 18 | button.addTarget(self, action: #selector(simulateLogOutThenLogInBehaviour), for: .touchUpInside) 19 | return button 20 | }() 21 | 22 | override func viewDidLoad() { 23 | super.viewDidLoad() 24 | 25 | title = "Memory Leaks Example" 26 | view.backgroundColor = .white 27 | 28 | let randomInt = Int.random(in: 1..<1000) 29 | randomNumberLabel.text = "Random number: \(randomInt)" 30 | 31 | setupViews() 32 | } 33 | 34 | private func setupViews() { 35 | view.addSubview(randomNumberLabel) 36 | view.addSubview(changeRootVCButton) 37 | 38 | randomNumberLabel.snp.makeConstraints { 39 | $0.centerX.centerY.equalToSuperview() 40 | } 41 | 42 | changeRootVCButton.snp.makeConstraints { 43 | $0.top.equalTo(randomNumberLabel.snp.bottom) 44 | $0.centerX.equalToSuperview() 45 | } 46 | } 47 | 48 | @objc 49 | private func simulateLogOutThenLogInBehaviour() { 50 | print("Simulate logout then login action") 51 | guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return } 52 | appDelegate.window?.rootViewController = HomeViewController() 53 | } 54 | } 55 | 56 | final class HomeViewModel { 57 | 58 | var callback: ((Int) -> Void)? 59 | var number: Int = 5 60 | 61 | init() { 62 | callback = { 63 | self.number = $0 64 | } 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /Docs/XCUITests.md: -------------------------------------------------------------------------------- 1 | 2 | # XCUITests 3 | 4 | After the tests finish execution, the essential thing that we need is the simulator needs to stay alive. 5 | The problem with `XCUITests` is that after testing finish execution, it quit the simulator. 6 | 7 | # Things I've tried: 8 | 9 | 1. Trying to find a way to preserve the running program after tests finish running 10 | 11 | - I can't find any way to preserve the running program. 12 | 13 | 2. At the end of a test, run shell script to generate `memgraph` before the running program quit 14 | 15 | For this idea, I put a breakpoint before the test finish execution. Then we can custom that breakpoint to execute a shell script command. 16 | However, when running on CI, we will execute test using script, not from Xcode. So, using breakpoint to execute shell script will not work for CI 17 | 18 | => Only work on Xcode 19 | 20 | 3. From Xcode13, Apple provide `-enablePerformanceTestsDiagnostics` to generate memgraph after a test finish executing. 21 | 22 | ```bash 23 | xcodebuild test -project MemoryLeaksCheck.xcodeproj \ 24 | -scheme LeaksCheckerUITests \ 25 | -destination platform=iOS,name="Tuan iPhone" \ 26 | -enablePerformanceTestsDiagnostics YES 27 | ``` 28 | 29 | > Note: In the scheme configuration, open `Options` under `Test`, unselect "Delete if test succeeds" for Attachments. 30 | 31 | 32 | 33 | However, **this only works for real device, not for simulator.** 34 | 35 | Based on [Apple docs](https://developer.apple.com/documentation/xcode-release-notes/xcode-13-release-notes) 36 | 37 | > xcodebuild has a new option -enablePerformanceTestsDiagnostics YES that collects diagnostics for Performance XCTests. The option collects a ktrace file for non-XCTMemoryMetrics, and a series of memory graphs for XCTMemoryMetrics. xcodebuild attaches diagnostics to the generated xcresult bundle. **Note that memory graph collection isn’t available in simulated devices. (64495534)** 38 | 39 | => Only work on local with real device, doesn't work on CI 40 | 41 | # Conclusion 42 | 43 | XCUITests is not appropriate for this approach. (for now) 44 | -------------------------------------------------------------------------------- /MemoryLeaksCheck/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // MemoryLeaksCheck 4 | // 5 | // Created by Hoang Anh Tuan on 23/09/2023. 6 | // 7 | 8 | import UIKit 9 | import SnapKit 10 | 11 | enum ScenarioType: String { 12 | case abandoned = "Abandoned Memory Example" 13 | case leaks = "Leaks Memory Example" 14 | } 15 | 16 | final class ViewController: UIViewController { 17 | 18 | private let tableView = UITableView(frame: .zero, style: .grouped) 19 | private let scenarios: [ScenarioType] = [.abandoned, .leaks] 20 | 21 | override func viewDidLoad() { 22 | super.viewDidLoad() 23 | setupViews() 24 | 25 | self.title = "Scenarios" 26 | view.backgroundColor = .white 27 | navigationController?.navigationBar.prefersLargeTitles = true 28 | navigationController?.navigationBar.isTranslucent = true 29 | } 30 | 31 | private func setupViews() { 32 | view.addSubview(tableView) 33 | 34 | tableView.snp.makeConstraints { 35 | $0.top.equalToSuperview() 36 | $0.leading.trailing.bottom.equalToSuperview() 37 | } 38 | 39 | tableView.dataSource = self 40 | tableView.delegate = self 41 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "CELL") 42 | tableView.rowHeight = UITableView.automaticDimension 43 | } 44 | } 45 | 46 | extension ViewController: UITableViewDataSource, UITableViewDelegate { 47 | 48 | public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 49 | scenarios.count 50 | } 51 | 52 | public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 53 | let cell = tableView.dequeueReusableCell(withIdentifier: "CELL", for: indexPath) 54 | cell.selectionStyle = .none 55 | cell.textLabel?.text = scenarios[indexPath.row].rawValue 56 | return cell 57 | } 58 | 59 | public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 60 | let scenario = scenarios[indexPath.row] 61 | switch scenario { 62 | case .abandoned: 63 | let vc = AbandonedMemoryVC() 64 | navigationController?.pushViewController(vc, animated: true) 65 | case .leaks: 66 | guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return } 67 | let vc = HomeViewController() 68 | appDelegate.window?.rootViewController = vc 69 | } 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /MemoryLeaksCheck.xcodeproj/xcshareddata/xcschemes/LeaksCheckerUITests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 16 | 17 | 21 | 22 | 26 | 27 | 28 | 29 | 31 | 37 | 38 | 39 | 40 | 41 | 51 | 52 | 56 | 57 | 61 | 62 | 63 | 64 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /MemoryLeaksCheck/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /LeaksDetector/Sources/LeaksDetector/CommandSteps/CheckLeaks.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Tuan Hoang on 6/1/24. 6 | // 7 | 8 | import Foundation 9 | import ShellOut 10 | 11 | enum LeaksCheckError: Error { 12 | case leaksCommandFailed 13 | case incorectOutputFormat 14 | } 15 | 16 | struct CheckLeaks: RunCommandStep { 17 | let executor: Executor 18 | 19 | private let regex: String = ".*(\\d+) leaks for (\\d+) total leaked bytes.*" 20 | 21 | func run() throws { 22 | 23 | if executor.memgraphPaths().isEmpty { 24 | log(message: "❌ Can not find any memgraphs!", color: .red) 25 | return 26 | } 27 | 28 | for memgraphPath in executor.memgraphPaths() { 29 | do { 30 | log(message: "Start checking for leaks... ⚙️") 31 | 32 | /// Running this script always throw error (somehow the leak tool throw error here) => So we need to process the memgraph in the `catch` block. 33 | try shellOut(to: "leaks", arguments: ["\(memgraphPath) -q"]) 34 | } catch { 35 | let error = error as! ShellOutError 36 | if error.output.isEmpty { 37 | log(message: "❌ Leaks command run failed! Please open an issue on Github for me to check!", color: .red) 38 | throw LeaksCheckError.leaksCommandFailed 39 | } 40 | 41 | let inputs = error.output.components(separatedBy: "\n") 42 | guard let numberOfLeaksMessage = inputs.first(where: { $0.matches(regex) }) else { 43 | log(message: "❌ Generated leaks output is incorrect! Please open an issue on Github for me to check!", color: .red) 44 | throw LeaksCheckError.incorectOutputFormat 45 | } 46 | 47 | let numberOfLeaks = getNumberOfLeaks(from: numberOfLeaksMessage) 48 | 49 | if numberOfLeaks < 1 { 50 | log(message: "Scan successfully. Don't find any leaks in the program! ✅", color: .green) 51 | return 52 | } 53 | 54 | for message in inputs { 55 | let updatedMessage = "\"\(message)\"" 56 | try shellOut(to: "echo \(updatedMessage) >> \(Constants.leaksReportFileName)") 57 | } 58 | 59 | log(message: "Founded leaks. Generating reports... ⚙️") 60 | } 61 | } 62 | } 63 | 64 | private func getMemgraphsURLs() -> [URL] { 65 | let fileManager = FileManager.default 66 | var memgraphFiles: [URL] = [] 67 | 68 | do { 69 | // Get the URLs of all items in the folder 70 | let files = try fileManager.contentsOfDirectory(at: Constants.memgraphsFolder, includingPropertiesForKeys: nil, options: []) 71 | 72 | // Filter the files that end with ".memgraph" 73 | memgraphFiles = files.filter { $0.pathExtension == "memgraph" } 74 | } catch { 75 | print("Failed to read contents of directory: \(error.localizedDescription)") 76 | } 77 | 78 | return memgraphFiles 79 | } 80 | 81 | private func getNumberOfLeaks(from message: String) -> Int { 82 | if let regex = try? NSRegularExpression(pattern: regex, options: []) { 83 | // Find the first match in the input string 84 | if let match = regex.firstMatch(in: message, options: [], range: NSRange(message.startIndex..., in: message)) { 85 | // Extract the "d" value from the first capture group 86 | if let dRange = Range(match.range(at: 1), in: message), let dValue = Int(message[dRange]) { 87 | return dValue 88 | } 89 | } 90 | } 91 | 92 | return 0 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # 🔎 CILeaksDetector 3 | 4 | ![Static Badge](https://img.shields.io/badge/status-active-brightgreen) 5 | 6 | This is a simple tool to help you checking for leaks in your program automatically in the most accurate way, then it will send the report directly to your development workflow (Pull request/Slack/...). 7 | And even better, you can easily integrate this tool to your CI workflow. 8 | 9 | 10 | 11 | # Table of Contents 12 | 13 | - [Installation](#installation) 14 | - [Usage](#usage) 15 | - [Current supported testing frameworks](#current-supported-testing-frameworks) 16 | - [How?](#how) 17 | - [How to support your testing framework](#how-to-support-your-testing-framework) 18 | - [Publication](#publication) 19 | 20 | ## Installation 21 | 22 | You can go to [GitHub Releases](https://github.com/hoangatuan/MemoryLeaksCheck/releases) page to download release executable program. 23 | 24 | ## Usage 25 | 26 | The most simple way to run the leaks checking process is: 27 | 28 | ```bash 29 | leaksdetector $subcommand -p $YOUR_APP_NAME 30 | ``` 31 | 32 | ### How to use subcommands 33 | 34 | Check out [this document](./Docs/Report.md) for how to use specific subcommands 35 | 36 | ### How to report the result to your development workflow 37 | 38 | Check out [this document](./Docs/Report.md) for how to customize the process to send the result to your workflow 39 | 40 | ## Current supported testing frameworks 41 | 42 | - [Maestro](https://maestro.mobile.dev/) ✅ 43 | - [XCUITest](https://developer.apple.com/documentation/xctest) ✅ (Only support real device. Read more [here](./Docs/XCUITests.md)) 44 | - [Appium](http://appium.io/) 🚧 (In progress) 45 | 46 | ### Why I used Maestro? 47 | 48 | 1. I need a testing tool which doesn't kill the program after the testing finished execution. And Maestro support that. Also Maestro is very easy to integrate & use. 49 | 2. XCUITest can not preserve running program after test execution on Simulator. Read more at [here](./Docs/XCUITests.md). However, Xcode support generate memgraph file for XCUITest when running test on real device => XCUITest is a great match for company uses real device farm for testing. 50 | 51 | ## How it works 52 | 53 | 1. Use a testing tool to simulate the UI flow in your app. 54 | 55 | 2. Generate `memgraph` using `leaks` tool provided by Apple. 56 | Find more about `leaks` tool and `memgraph` [here](https://developer.apple.com/videos/play/wwdc2018/416/) 57 | 58 | 3. Use `leaksdetector` program to proceed the `memgraph` file. If any leaks founded, it will use Danger to post a message to your PR/slack, ... 59 | 60 | ## How to support your testing framework 61 | 62 | If you're using another UI testing framework which also support preserve the execution of the program after finish testing, you can create another PR to update the `leaksdetector`. 63 | It's easy to do that, just need to follow these steps: 64 | 65 | 1. Open `Executor.swift`, create a new instance of your testing frameworks. Your new instance needs to conform to `Executor` protocol. 66 | 67 | ```swift 68 | 69 | struct XCUITestExecutor: Executor { 70 | 71 | func simulateUI() throws { 72 | // Custom logic to start simulating UI 73 | } 74 | 75 | func generateMemgraph(for processName: String) throws { 76 | // Custom logic to start generating memgraph for a `processName` 77 | } 78 | 79 | func memgraphPaths() -> [String] 80 | // return the path to the generated memgraphs 81 | } 82 | } 83 | 84 | ``` 85 | 86 | 2. Go to `Commands` folder to create a new command for your testing framework. Please refer to `MaestroCommand`. 87 | 88 | ## Publication 89 | 90 | I've published an article about this on Medium. You can take a look at [here](https://medium.com/gitconnected/automating-memory-leak-detection-with-ci-integration-for-ios-380f08a55f0b) 91 | -------------------------------------------------------------------------------- /MemoryLeaksCheck.xcodeproj/xcshareddata/xcschemes/MemoryLeaksCheck.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 35 | 36 | 40 | 41 | 42 | 43 | 45 | 51 | 52 | 53 | 54 | 55 | 65 | 67 | 73 | 74 | 75 | 76 | 80 | 81 | 85 | 86 | 87 | 88 | 94 | 96 | 102 | 103 | 104 | 105 | 107 | 108 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /LeaksDetector/.swiftpm/xcode/xcshareddata/xcschemes/LeaksDetector.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | 61 | 67 | 68 | 69 | 70 | 71 | 82 | 84 | 90 | 91 | 92 | 93 | 99 | 101 | 107 | 108 | 109 | 110 | 112 | 113 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /LeaksDetector/Sources/LeaksDetector/Executor/XCUITestExecutor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Tuan Hoang Anh on 18/8/24. 6 | // 7 | 8 | import Foundation 9 | import ShellOut 10 | import XCResultKit 11 | 12 | final class XCUITestExecutor: Executor { 13 | 14 | private let xcResultPath = "./TestResults" 15 | 16 | enum ProjectType { 17 | case workspace(path: String) 18 | case project(path: String) 19 | 20 | var argument: String { 21 | switch self { 22 | case .workspace(let path): 23 | return "-workspace \(path)" 24 | case .project(let path): 25 | return "-project \(path)" 26 | } 27 | } 28 | } 29 | 30 | let projectType: ProjectType 31 | let scheme: String 32 | let destination: String 33 | 34 | init( 35 | project: String?, 36 | workspace: String?, 37 | scheme: String, 38 | destination: String 39 | ) throws { 40 | 41 | if let project = project { 42 | self.projectType = .project(path: project) 43 | } else if let workspace = workspace { 44 | self.projectType = .workspace(path: workspace) 45 | } else { 46 | throw NSError(domain: "XCUITestExecutor", code: 0, userInfo: ["message": "Either project or workspace must be provided"]) 47 | } 48 | 49 | self.scheme = scheme 50 | self.destination = destination 51 | } 52 | 53 | func simulateUI() throws { 54 | try shellOut( 55 | to: "xcodebuild", 56 | arguments: [ 57 | "test", 58 | projectType.argument, 59 | "-scheme", 60 | scheme, 61 | "-destination", 62 | destination, 63 | "-enablePerformanceTestsDiagnostics", 64 | "YES", 65 | "-derivedDataPath", 66 | "./derived_data", 67 | "-resultBundlePath", 68 | xcResultPath, 69 | ] 70 | ) 71 | } 72 | 73 | /// XCUITest already generate memgraph by default, so in this step we just need to copy the memgraph file to the correct folder 74 | func generateMemgraph(for processName: String) throws { 75 | /// https://www.polpiella.dev/automatically-detect-memory-leaks-using-ui-tests/?utm_campaign=iOS%20CI%20Newsletter&utm_medium=email&utm_source=iOS%20CI%20Newsletter%20Issue%2048&utm_content=aug_11_24 76 | guard let url = URL(string: xcResultPath) else { return } 77 | let result = XCResultFile(url: url) 78 | guard let invocationRecord = result.getInvocationRecord() else { return } 79 | 80 | let testBundles = invocationRecord 81 | .actions 82 | .compactMap { action -> ActionTestPlanRunSummaries? in 83 | guard let id = action.actionResult.testsRef?.id, let summaries = result.getTestPlanRunSummaries(id: id) else { 84 | return nil 85 | } 86 | 87 | return summaries 88 | } 89 | .flatMap(\.summaries) 90 | .flatMap(\.testableSummaries) 91 | 92 | let allTests = testBundles 93 | .flatMap(\.tests) 94 | .flatMap(\.subtestGroups) 95 | .flatMap(\.subtestGroups) 96 | .flatMap(\.subtests) 97 | 98 | let memoryGraphAttachments = allTests 99 | .compactMap { test -> ActionTestSummary? in 100 | guard let id = test.summaryRef?.id else { return nil } 101 | return result.getActionTestSummary(id: id) 102 | } 103 | .flatMap(\.activitySummaries) 104 | .filter { $0.title.contains("Added attachment named") && $0.title.contains(".memgraphset.zip") } 105 | .flatMap(\.attachments) 106 | 107 | for attachment in memoryGraphAttachments { 108 | let url = Constants.memgraphsFolder 109 | let filePath = url.appending(path: attachment.filename ?? "") 110 | result.exportAttachment(attachment: attachment, outputPath: url.path(percentEncoded: false)) 111 | 112 | try shellOut( 113 | to: "tar", 114 | arguments: [ 115 | "-zxvf", 116 | "\"\(filePath.path(percentEncoded: false))\"", 117 | "-C", 118 | url.path(percentEncoded: false) 119 | ] 120 | ) 121 | } 122 | } 123 | 124 | func memgraphPaths() -> [String] { 125 | let fileManager = FileManager.default 126 | var memgraphFiles: [URL] = [] 127 | 128 | // Create a directory enumerator to recursively go through the folder 129 | if let enumerator = fileManager.enumerator(at: Constants.memgraphsFolder, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]) { 130 | // Iterate through the files and directories 131 | for case let fileURL as URL in enumerator { 132 | if fileURL.pathExtension == "memgraph" { 133 | memgraphFiles.append(fileURL) 134 | } 135 | } 136 | } 137 | 138 | let postMemgraphs: [String] = memgraphFiles 139 | .map { 140 | let unzippedAndEscaped = ($0.path(percentEncoded: false) as NSString) 141 | .replacingOccurrences(of: "(", with: "\\(") 142 | .replacingOccurrences(of: ")", with: "\\)") 143 | .replacingOccurrences(of: "[", with: "\\[") 144 | .replacingOccurrences(of: "]", with: "\\]") 145 | 146 | return unzippedAndEscaped 147 | } 148 | .filter { 149 | $0.contains("post_") 150 | } 151 | 152 | return postMemgraphs 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /MemoryLeaksCheck.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 4C28E3CE2ABEDFFB0004394E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C28E3CD2ABEDFFB0004394E /* AppDelegate.swift */; }; 11 | 4C28E3D22ABEDFFB0004394E /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C28E3D12ABEDFFB0004394E /* ViewController.swift */; }; 12 | 4C28E3D52ABEDFFB0004394E /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4C28E3D32ABEDFFB0004394E /* Main.storyboard */; }; 13 | 4C28E3D72ABEE0030004394E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4C28E3D62ABEE0030004394E /* Assets.xcassets */; }; 14 | 4C28E3DA2ABEE0030004394E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4C28E3D82ABEE0030004394E /* LaunchScreen.storyboard */; }; 15 | 4C28E3E32ABEE2D90004394E /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4C28E3E22ABEE2D90004394E /* SnapKit */; }; 16 | 4C28E3E62ABEE50B0004394E /* AbandonedMemoryExampleVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C28E3E52ABEE50B0004394E /* AbandonedMemoryExampleVC.swift */; }; 17 | 4C28E3E82ABF1DED0004394E /* LeaksExampleVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C28E3E72ABF1DED0004394E /* LeaksExampleVC.swift */; }; 18 | 4CBC593B2AC3299B00D83CA5 /* LeaksCheckerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBC593A2AC3299B00D83CA5 /* LeaksCheckerUITests.swift */; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXContainerItemProxy section */ 22 | 4CBC593E2AC3299B00D83CA5 /* PBXContainerItemProxy */ = { 23 | isa = PBXContainerItemProxy; 24 | containerPortal = 4C28E3C22ABEDFFB0004394E /* Project object */; 25 | proxyType = 1; 26 | remoteGlobalIDString = 4C28E3C92ABEDFFB0004394E; 27 | remoteInfo = MemoryLeaksCheck; 28 | }; 29 | /* End PBXContainerItemProxy section */ 30 | 31 | /* Begin PBXFileReference section */ 32 | 4C28E3CA2ABEDFFB0004394E /* MemoryLeaksCheck.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MemoryLeaksCheck.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33 | 4C28E3CD2ABEDFFB0004394E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 34 | 4C28E3D12ABEDFFB0004394E /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 35 | 4C28E3D42ABEDFFB0004394E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 36 | 4C28E3D62ABEE0030004394E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 37 | 4C28E3D92ABEE0030004394E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 38 | 4C28E3DB2ABEE0030004394E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 39 | 4C28E3E52ABEE50B0004394E /* AbandonedMemoryExampleVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbandonedMemoryExampleVC.swift; sourceTree = ""; }; 40 | 4C28E3E72ABF1DED0004394E /* LeaksExampleVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeaksExampleVC.swift; sourceTree = ""; }; 41 | 4CBC59382AC3299B00D83CA5 /* LeaksCheckerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LeaksCheckerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 42 | 4CBC593A2AC3299B00D83CA5 /* LeaksCheckerUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeaksCheckerUITests.swift; sourceTree = ""; }; 43 | /* End PBXFileReference section */ 44 | 45 | /* Begin PBXFrameworksBuildPhase section */ 46 | 4C28E3C72ABEDFFB0004394E /* Frameworks */ = { 47 | isa = PBXFrameworksBuildPhase; 48 | buildActionMask = 2147483647; 49 | files = ( 50 | 4C28E3E32ABEE2D90004394E /* SnapKit in Frameworks */, 51 | ); 52 | runOnlyForDeploymentPostprocessing = 0; 53 | }; 54 | 4CBC59352AC3299B00D83CA5 /* Frameworks */ = { 55 | isa = PBXFrameworksBuildPhase; 56 | buildActionMask = 2147483647; 57 | files = ( 58 | ); 59 | runOnlyForDeploymentPostprocessing = 0; 60 | }; 61 | /* End PBXFrameworksBuildPhase section */ 62 | 63 | /* Begin PBXGroup section */ 64 | 4C28E3C12ABEDFFB0004394E = { 65 | isa = PBXGroup; 66 | children = ( 67 | 4C28E3CC2ABEDFFB0004394E /* MemoryLeaksCheck */, 68 | 4CBC59392AC3299B00D83CA5 /* LeaksCheckerUITests */, 69 | 4C28E3CB2ABEDFFB0004394E /* Products */, 70 | ); 71 | sourceTree = ""; 72 | }; 73 | 4C28E3CB2ABEDFFB0004394E /* Products */ = { 74 | isa = PBXGroup; 75 | children = ( 76 | 4C28E3CA2ABEDFFB0004394E /* MemoryLeaksCheck.app */, 77 | 4CBC59382AC3299B00D83CA5 /* LeaksCheckerUITests.xctest */, 78 | ); 79 | name = Products; 80 | sourceTree = ""; 81 | }; 82 | 4C28E3CC2ABEDFFB0004394E /* MemoryLeaksCheck */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | 4C28E3E42ABEE4FE0004394E /* Scenarios */, 86 | 4C28E3CD2ABEDFFB0004394E /* AppDelegate.swift */, 87 | 4C28E3D12ABEDFFB0004394E /* ViewController.swift */, 88 | 4C28E3D32ABEDFFB0004394E /* Main.storyboard */, 89 | 4C28E3D62ABEE0030004394E /* Assets.xcassets */, 90 | 4C28E3D82ABEE0030004394E /* LaunchScreen.storyboard */, 91 | 4C28E3DB2ABEE0030004394E /* Info.plist */, 92 | ); 93 | path = MemoryLeaksCheck; 94 | sourceTree = ""; 95 | }; 96 | 4C28E3E42ABEE4FE0004394E /* Scenarios */ = { 97 | isa = PBXGroup; 98 | children = ( 99 | 4C28E3E52ABEE50B0004394E /* AbandonedMemoryExampleVC.swift */, 100 | 4C28E3E72ABF1DED0004394E /* LeaksExampleVC.swift */, 101 | ); 102 | path = Scenarios; 103 | sourceTree = ""; 104 | }; 105 | 4CBC59392AC3299B00D83CA5 /* LeaksCheckerUITests */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | 4CBC593A2AC3299B00D83CA5 /* LeaksCheckerUITests.swift */, 109 | ); 110 | path = LeaksCheckerUITests; 111 | sourceTree = ""; 112 | }; 113 | /* End PBXGroup section */ 114 | 115 | /* Begin PBXNativeTarget section */ 116 | 4C28E3C92ABEDFFB0004394E /* MemoryLeaksCheck */ = { 117 | isa = PBXNativeTarget; 118 | buildConfigurationList = 4C28E3DE2ABEE0030004394E /* Build configuration list for PBXNativeTarget "MemoryLeaksCheck" */; 119 | buildPhases = ( 120 | 4C28E3C62ABEDFFB0004394E /* Sources */, 121 | 4C28E3C72ABEDFFB0004394E /* Frameworks */, 122 | 4C28E3C82ABEDFFB0004394E /* Resources */, 123 | ); 124 | buildRules = ( 125 | ); 126 | dependencies = ( 127 | ); 128 | name = MemoryLeaksCheck; 129 | packageProductDependencies = ( 130 | 4C28E3E22ABEE2D90004394E /* SnapKit */, 131 | ); 132 | productName = MemoryLeaksCheck; 133 | productReference = 4C28E3CA2ABEDFFB0004394E /* MemoryLeaksCheck.app */; 134 | productType = "com.apple.product-type.application"; 135 | }; 136 | 4CBC59372AC3299B00D83CA5 /* LeaksCheckerUITests */ = { 137 | isa = PBXNativeTarget; 138 | buildConfigurationList = 4CBC59402AC3299B00D83CA5 /* Build configuration list for PBXNativeTarget "LeaksCheckerUITests" */; 139 | buildPhases = ( 140 | 4CBC59342AC3299B00D83CA5 /* Sources */, 141 | 4CBC59352AC3299B00D83CA5 /* Frameworks */, 142 | 4CBC59362AC3299B00D83CA5 /* Resources */, 143 | ); 144 | buildRules = ( 145 | ); 146 | dependencies = ( 147 | 4CBC593F2AC3299B00D83CA5 /* PBXTargetDependency */, 148 | ); 149 | name = LeaksCheckerUITests; 150 | productName = LeaksCheckerUITests; 151 | productReference = 4CBC59382AC3299B00D83CA5 /* LeaksCheckerUITests.xctest */; 152 | productType = "com.apple.product-type.bundle.ui-testing"; 153 | }; 154 | /* End PBXNativeTarget section */ 155 | 156 | /* Begin PBXProject section */ 157 | 4C28E3C22ABEDFFB0004394E /* Project object */ = { 158 | isa = PBXProject; 159 | attributes = { 160 | BuildIndependentTargetsInParallel = 1; 161 | LastSwiftUpdateCheck = 1420; 162 | LastUpgradeCheck = 1420; 163 | TargetAttributes = { 164 | 4C28E3C92ABEDFFB0004394E = { 165 | CreatedOnToolsVersion = 14.2; 166 | }; 167 | 4CBC59372AC3299B00D83CA5 = { 168 | CreatedOnToolsVersion = 14.2; 169 | TestTargetID = 4C28E3C92ABEDFFB0004394E; 170 | }; 171 | }; 172 | }; 173 | buildConfigurationList = 4C28E3C52ABEDFFB0004394E /* Build configuration list for PBXProject "MemoryLeaksCheck" */; 174 | compatibilityVersion = "Xcode 14.0"; 175 | developmentRegion = en; 176 | hasScannedForEncodings = 0; 177 | knownRegions = ( 178 | en, 179 | Base, 180 | ); 181 | mainGroup = 4C28E3C12ABEDFFB0004394E; 182 | packageReferences = ( 183 | 4C28E3E12ABEE2D90004394E /* XCRemoteSwiftPackageReference "SnapKit" */, 184 | ); 185 | productRefGroup = 4C28E3CB2ABEDFFB0004394E /* Products */; 186 | projectDirPath = ""; 187 | projectRoot = ""; 188 | targets = ( 189 | 4C28E3C92ABEDFFB0004394E /* MemoryLeaksCheck */, 190 | 4CBC59372AC3299B00D83CA5 /* LeaksCheckerUITests */, 191 | ); 192 | }; 193 | /* End PBXProject section */ 194 | 195 | /* Begin PBXResourcesBuildPhase section */ 196 | 4C28E3C82ABEDFFB0004394E /* Resources */ = { 197 | isa = PBXResourcesBuildPhase; 198 | buildActionMask = 2147483647; 199 | files = ( 200 | 4C28E3DA2ABEE0030004394E /* LaunchScreen.storyboard in Resources */, 201 | 4C28E3D72ABEE0030004394E /* Assets.xcassets in Resources */, 202 | 4C28E3D52ABEDFFB0004394E /* Main.storyboard in Resources */, 203 | ); 204 | runOnlyForDeploymentPostprocessing = 0; 205 | }; 206 | 4CBC59362AC3299B00D83CA5 /* Resources */ = { 207 | isa = PBXResourcesBuildPhase; 208 | buildActionMask = 2147483647; 209 | files = ( 210 | ); 211 | runOnlyForDeploymentPostprocessing = 0; 212 | }; 213 | /* End PBXResourcesBuildPhase section */ 214 | 215 | /* Begin PBXSourcesBuildPhase section */ 216 | 4C28E3C62ABEDFFB0004394E /* Sources */ = { 217 | isa = PBXSourcesBuildPhase; 218 | buildActionMask = 2147483647; 219 | files = ( 220 | 4C28E3E82ABF1DED0004394E /* LeaksExampleVC.swift in Sources */, 221 | 4C28E3D22ABEDFFB0004394E /* ViewController.swift in Sources */, 222 | 4C28E3E62ABEE50B0004394E /* AbandonedMemoryExampleVC.swift in Sources */, 223 | 4C28E3CE2ABEDFFB0004394E /* AppDelegate.swift in Sources */, 224 | ); 225 | runOnlyForDeploymentPostprocessing = 0; 226 | }; 227 | 4CBC59342AC3299B00D83CA5 /* Sources */ = { 228 | isa = PBXSourcesBuildPhase; 229 | buildActionMask = 2147483647; 230 | files = ( 231 | 4CBC593B2AC3299B00D83CA5 /* LeaksCheckerUITests.swift in Sources */, 232 | ); 233 | runOnlyForDeploymentPostprocessing = 0; 234 | }; 235 | /* End PBXSourcesBuildPhase section */ 236 | 237 | /* Begin PBXTargetDependency section */ 238 | 4CBC593F2AC3299B00D83CA5 /* PBXTargetDependency */ = { 239 | isa = PBXTargetDependency; 240 | target = 4C28E3C92ABEDFFB0004394E /* MemoryLeaksCheck */; 241 | targetProxy = 4CBC593E2AC3299B00D83CA5 /* PBXContainerItemProxy */; 242 | }; 243 | /* End PBXTargetDependency section */ 244 | 245 | /* Begin PBXVariantGroup section */ 246 | 4C28E3D32ABEDFFB0004394E /* Main.storyboard */ = { 247 | isa = PBXVariantGroup; 248 | children = ( 249 | 4C28E3D42ABEDFFB0004394E /* Base */, 250 | ); 251 | name = Main.storyboard; 252 | sourceTree = ""; 253 | }; 254 | 4C28E3D82ABEE0030004394E /* LaunchScreen.storyboard */ = { 255 | isa = PBXVariantGroup; 256 | children = ( 257 | 4C28E3D92ABEE0030004394E /* Base */, 258 | ); 259 | name = LaunchScreen.storyboard; 260 | sourceTree = ""; 261 | }; 262 | /* End PBXVariantGroup section */ 263 | 264 | /* Begin XCBuildConfiguration section */ 265 | 4C28E3DC2ABEE0030004394E /* Debug */ = { 266 | isa = XCBuildConfiguration; 267 | buildSettings = { 268 | ALWAYS_SEARCH_USER_PATHS = NO; 269 | CLANG_ANALYZER_NONNULL = YES; 270 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 271 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 272 | CLANG_ENABLE_MODULES = YES; 273 | CLANG_ENABLE_OBJC_ARC = YES; 274 | CLANG_ENABLE_OBJC_WEAK = YES; 275 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 276 | CLANG_WARN_BOOL_CONVERSION = YES; 277 | CLANG_WARN_COMMA = YES; 278 | CLANG_WARN_CONSTANT_CONVERSION = YES; 279 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 280 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 281 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 282 | CLANG_WARN_EMPTY_BODY = YES; 283 | CLANG_WARN_ENUM_CONVERSION = YES; 284 | CLANG_WARN_INFINITE_RECURSION = YES; 285 | CLANG_WARN_INT_CONVERSION = YES; 286 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 287 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 288 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 289 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 290 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 291 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 292 | CLANG_WARN_STRICT_PROTOTYPES = YES; 293 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 294 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 295 | CLANG_WARN_UNREACHABLE_CODE = YES; 296 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 297 | COPY_PHASE_STRIP = NO; 298 | DEBUG_INFORMATION_FORMAT = dwarf; 299 | ENABLE_STRICT_OBJC_MSGSEND = YES; 300 | ENABLE_TESTABILITY = YES; 301 | GCC_C_LANGUAGE_STANDARD = gnu11; 302 | GCC_DYNAMIC_NO_PIC = NO; 303 | GCC_NO_COMMON_BLOCKS = YES; 304 | GCC_OPTIMIZATION_LEVEL = 0; 305 | GCC_PREPROCESSOR_DEFINITIONS = ( 306 | "DEBUG=1", 307 | "$(inherited)", 308 | ); 309 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 310 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 311 | GCC_WARN_UNDECLARED_SELECTOR = YES; 312 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 313 | GCC_WARN_UNUSED_FUNCTION = YES; 314 | GCC_WARN_UNUSED_VARIABLE = YES; 315 | IPHONEOS_DEPLOYMENT_TARGET = 16.2; 316 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 317 | MTL_FAST_MATH = YES; 318 | ONLY_ACTIVE_ARCH = YES; 319 | SDKROOT = iphoneos; 320 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 321 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 322 | }; 323 | name = Debug; 324 | }; 325 | 4C28E3DD2ABEE0030004394E /* Release */ = { 326 | isa = XCBuildConfiguration; 327 | buildSettings = { 328 | ALWAYS_SEARCH_USER_PATHS = NO; 329 | CLANG_ANALYZER_NONNULL = YES; 330 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 331 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 332 | CLANG_ENABLE_MODULES = YES; 333 | CLANG_ENABLE_OBJC_ARC = YES; 334 | CLANG_ENABLE_OBJC_WEAK = YES; 335 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 336 | CLANG_WARN_BOOL_CONVERSION = YES; 337 | CLANG_WARN_COMMA = YES; 338 | CLANG_WARN_CONSTANT_CONVERSION = YES; 339 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 340 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 341 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 342 | CLANG_WARN_EMPTY_BODY = YES; 343 | CLANG_WARN_ENUM_CONVERSION = YES; 344 | CLANG_WARN_INFINITE_RECURSION = YES; 345 | CLANG_WARN_INT_CONVERSION = YES; 346 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 347 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 348 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 349 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 350 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 351 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 352 | CLANG_WARN_STRICT_PROTOTYPES = YES; 353 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 354 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 355 | CLANG_WARN_UNREACHABLE_CODE = YES; 356 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 357 | COPY_PHASE_STRIP = NO; 358 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 359 | ENABLE_NS_ASSERTIONS = NO; 360 | ENABLE_STRICT_OBJC_MSGSEND = YES; 361 | GCC_C_LANGUAGE_STANDARD = gnu11; 362 | GCC_NO_COMMON_BLOCKS = YES; 363 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 364 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 365 | GCC_WARN_UNDECLARED_SELECTOR = YES; 366 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 367 | GCC_WARN_UNUSED_FUNCTION = YES; 368 | GCC_WARN_UNUSED_VARIABLE = YES; 369 | IPHONEOS_DEPLOYMENT_TARGET = 16.2; 370 | MTL_ENABLE_DEBUG_INFO = NO; 371 | MTL_FAST_MATH = YES; 372 | SDKROOT = iphoneos; 373 | SWIFT_COMPILATION_MODE = wholemodule; 374 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 375 | VALIDATE_PRODUCT = YES; 376 | }; 377 | name = Release; 378 | }; 379 | 4C28E3DF2ABEE0030004394E /* Debug */ = { 380 | isa = XCBuildConfiguration; 381 | buildSettings = { 382 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 383 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 384 | CODE_SIGN_STYLE = Automatic; 385 | CURRENT_PROJECT_VERSION = 1; 386 | DEVELOPMENT_TEAM = RCQA85T6C2; 387 | GENERATE_INFOPLIST_FILE = YES; 388 | INFOPLIST_FILE = MemoryLeaksCheck/Info.plist; 389 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 390 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 391 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 392 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 393 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 394 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 395 | LD_RUNPATH_SEARCH_PATHS = ( 396 | "$(inherited)", 397 | "@executable_path/Frameworks", 398 | ); 399 | MARKETING_VERSION = 1.0; 400 | PRODUCT_BUNDLE_IDENTIFIER = "Hoang-Anh-Tuan.MemoryLeaksCheck"; 401 | PRODUCT_NAME = "$(TARGET_NAME)"; 402 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 403 | SUPPORTS_MACCATALYST = NO; 404 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 405 | SWIFT_EMIT_LOC_STRINGS = YES; 406 | SWIFT_VERSION = 5.0; 407 | TARGETED_DEVICE_FAMILY = 1; 408 | }; 409 | name = Debug; 410 | }; 411 | 4C28E3E02ABEE0030004394E /* Release */ = { 412 | isa = XCBuildConfiguration; 413 | buildSettings = { 414 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 415 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 416 | CODE_SIGN_STYLE = Automatic; 417 | CURRENT_PROJECT_VERSION = 1; 418 | DEVELOPMENT_TEAM = RCQA85T6C2; 419 | GENERATE_INFOPLIST_FILE = YES; 420 | INFOPLIST_FILE = MemoryLeaksCheck/Info.plist; 421 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 422 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 423 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 424 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 425 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 426 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 427 | LD_RUNPATH_SEARCH_PATHS = ( 428 | "$(inherited)", 429 | "@executable_path/Frameworks", 430 | ); 431 | MARKETING_VERSION = 1.0; 432 | PRODUCT_BUNDLE_IDENTIFIER = "Hoang-Anh-Tuan.MemoryLeaksCheck"; 433 | PRODUCT_NAME = "$(TARGET_NAME)"; 434 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 435 | SUPPORTS_MACCATALYST = NO; 436 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 437 | SWIFT_EMIT_LOC_STRINGS = YES; 438 | SWIFT_VERSION = 5.0; 439 | TARGETED_DEVICE_FAMILY = 1; 440 | }; 441 | name = Release; 442 | }; 443 | 4CBC59412AC3299B00D83CA5 /* Debug */ = { 444 | isa = XCBuildConfiguration; 445 | buildSettings = { 446 | CODE_SIGN_STYLE = Automatic; 447 | CURRENT_PROJECT_VERSION = 1; 448 | DEVELOPMENT_TEAM = RCQA85T6C2; 449 | GENERATE_INFOPLIST_FILE = YES; 450 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 451 | MARKETING_VERSION = 1.0; 452 | PRODUCT_BUNDLE_IDENTIFIER = "Hoang-Anh-Tuan.LeaksCheckerUITests"; 453 | PRODUCT_NAME = "$(TARGET_NAME)"; 454 | PROVISIONING_PROFILE = ""; 455 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 456 | SUPPORTS_MACCATALYST = NO; 457 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 458 | SWIFT_EMIT_LOC_STRINGS = NO; 459 | SWIFT_VERSION = 5.0; 460 | TARGETED_DEVICE_FAMILY = 1; 461 | TEST_TARGET_NAME = MemoryLeaksCheck; 462 | }; 463 | name = Debug; 464 | }; 465 | 4CBC59422AC3299B00D83CA5 /* Release */ = { 466 | isa = XCBuildConfiguration; 467 | buildSettings = { 468 | CODE_SIGN_STYLE = Automatic; 469 | CURRENT_PROJECT_VERSION = 1; 470 | DEVELOPMENT_TEAM = RCQA85T6C2; 471 | GENERATE_INFOPLIST_FILE = YES; 472 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 473 | MARKETING_VERSION = 1.0; 474 | PRODUCT_BUNDLE_IDENTIFIER = "Hoang-Anh-Tuan.LeaksCheckerUITests"; 475 | PRODUCT_NAME = "$(TARGET_NAME)"; 476 | PROVISIONING_PROFILE = ""; 477 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 478 | SUPPORTS_MACCATALYST = NO; 479 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 480 | SWIFT_EMIT_LOC_STRINGS = NO; 481 | SWIFT_VERSION = 5.0; 482 | TARGETED_DEVICE_FAMILY = 1; 483 | TEST_TARGET_NAME = MemoryLeaksCheck; 484 | }; 485 | name = Release; 486 | }; 487 | /* End XCBuildConfiguration section */ 488 | 489 | /* Begin XCConfigurationList section */ 490 | 4C28E3C52ABEDFFB0004394E /* Build configuration list for PBXProject "MemoryLeaksCheck" */ = { 491 | isa = XCConfigurationList; 492 | buildConfigurations = ( 493 | 4C28E3DC2ABEE0030004394E /* Debug */, 494 | 4C28E3DD2ABEE0030004394E /* Release */, 495 | ); 496 | defaultConfigurationIsVisible = 0; 497 | defaultConfigurationName = Release; 498 | }; 499 | 4C28E3DE2ABEE0030004394E /* Build configuration list for PBXNativeTarget "MemoryLeaksCheck" */ = { 500 | isa = XCConfigurationList; 501 | buildConfigurations = ( 502 | 4C28E3DF2ABEE0030004394E /* Debug */, 503 | 4C28E3E02ABEE0030004394E /* Release */, 504 | ); 505 | defaultConfigurationIsVisible = 0; 506 | defaultConfigurationName = Release; 507 | }; 508 | 4CBC59402AC3299B00D83CA5 /* Build configuration list for PBXNativeTarget "LeaksCheckerUITests" */ = { 509 | isa = XCConfigurationList; 510 | buildConfigurations = ( 511 | 4CBC59412AC3299B00D83CA5 /* Debug */, 512 | 4CBC59422AC3299B00D83CA5 /* Release */, 513 | ); 514 | defaultConfigurationIsVisible = 0; 515 | defaultConfigurationName = Release; 516 | }; 517 | /* End XCConfigurationList section */ 518 | 519 | /* Begin XCRemoteSwiftPackageReference section */ 520 | 4C28E3E12ABEE2D90004394E /* XCRemoteSwiftPackageReference "SnapKit" */ = { 521 | isa = XCRemoteSwiftPackageReference; 522 | repositoryURL = "https://github.com/SnapKit/SnapKit"; 523 | requirement = { 524 | branch = develop; 525 | kind = branch; 526 | }; 527 | }; 528 | /* End XCRemoteSwiftPackageReference section */ 529 | 530 | /* Begin XCSwiftPackageProductDependency section */ 531 | 4C28E3E22ABEE2D90004394E /* SnapKit */ = { 532 | isa = XCSwiftPackageProductDependency; 533 | package = 4C28E3E12ABEE2D90004394E /* XCRemoteSwiftPackageReference "SnapKit" */; 534 | productName = SnapKit; 535 | }; 536 | /* End XCSwiftPackageProductDependency section */ 537 | }; 538 | rootObject = 4C28E3C22ABEDFFB0004394E /* Project object */; 539 | } 540 | --------------------------------------------------------------------------------