iOS Developer là chuyên gia tạo ứng dụng cho hệ điều hành iOS của Apple. Những nhà phát triển này có hiểu biết sâu sắc về hệ sinh thái iOS. Bài viết sau đây sẽ tổng hợp các câu hỏi phỏng vấn iOS Developer phổ biến, bao gồm các chủ đề về framework, hướng dẫn thiết kế và ngôn ngữ lập trình, chủ yếu là Swift và Objective-C.

Bài viết này sẽ tập trung tổng hợp các câu hỏi phỏng vấn iOS Developer có liên quan đến các chủ đề:

  • Các câu hỏi phỏng vấn iOS Developer liên quan đến Swift và Objective-C
  • Các câu hỏi phỏng vấn iOS Developer liên quan đến iOS SDK
  • Các câu hỏi phỏng vấn iOS Developer liên quan đến UI/UX và kiến trúc
  • Các câu hỏi phỏng vấn iOS Developer tối ưu hoá hiệu năng

iOS Developer là ai? Công việc của iOS Developer là gì?

iOS Developer chịu trách nhiệm thiết kế, xây dựng, kiểm thử và duy trì các ứng dụng di động trên nền tảng iOS, đảm bảo chúng đáp ứng nhu cầu người dùng và mang lại trải nghiệm mượt mà.

Họ cần thành thạo ngôn ngữ lập trình Swift và Objective-C, hiểu biết sâu về kiến trúc iOS, các nguyên lý phát triển ứng dụng di động, và có kinh nghiệm tích hợp API của bên thứ ba.

Bên cạnh kỹ năng chuyên môn, iOS Developer cần khả năng giải quyết vấn đề, làm việc nhóm và cập nhật nhanh các xu hướng công nghệ mới. Các công cụ cần thiết cho công việc này bao gồm Xcode, Git, và các thư viện như Alamofire, CoreData, cùng với các công cụ kiểm thử và CI/CD để hỗ trợ phát triển ứng dụng hiệu quả.

Trước khi đi vào danh sách các câu hỏi phỏng vấn iOS Developer sau, bạn nên “ôn tập” kiến thức với các bài viết sau đây:

Nếu bạn là Android Developer, bạn nên tham khảo Top 40+ câu hỏi phỏng vấn Android Developer.

Các câu hỏi phỏng vấn iOS Developer liên quan đến Swift và Objective-C

Bạn đã từng làm việc với cả Swift và Objective-C chưa? Nếu có, bạn có thể so sánh cách xử lý bộ nhớ trong hai ngôn ngữ này không?

Trong Objective-C, bộ nhớ được quản lý thủ công bằng cách sử dụng retain/release hoặc autorelease, điều này dễ dẫn đến rò rỉ bộ nhớ nếu không cẩn thận.

Trong khi đó, Swift sử dụng ARC (Automatic Reference Counting), giúp tự động quản lý bộ nhớ bằng cách theo dõi số tham chiếu đến các đối tượng và giải phóng chúng khi không còn được sử dụng. Điều này giúp đơn giản hóa quy trình phát triển và giảm thiểu lỗi liên quan đến quản lý bộ nhớ.

Sự khác biệt giữa class và struct trong Swift là gì?

Trong Swift, classstruct đều được sử dụng để tạo ra các kiểu dữ liệu tùy chỉnh, nhưng có một số khác biệt chính:

Đặc điểm  Class  Struct 
Kiểu Tham chiếu (Reference type) Giá trị (Value type)
Kế thừa Hỗ trợ kế thừa  Không hỗ trợ kế thừa 
Deinitializer Có thể có deinitializers Không có deinitializers
Mutability Có thể thay đổi thuộc tính khi biến được khai báo là var và có thể thay đổi ngay cả khi là let trong một phạm vi không thay đổi. Không thể thay đổi thuộc tính khi biến được khai báo là let
Sao chép Sao chép tham chiếu đến đối tượng (cả hai biến đều tham chiếu cùng một đối tượng) Sao chép giá trị (tạo bản sao độc lập)
Cấu trúc dữ liệu Thích hợp cho các đối tượng phức tạp, cần kế thừa và chia sẻ trạng thái. Thích hợp cho các kiểu dữ liệu đơn giản, không cần chia sẻ trạng thái.

Giải thích từ khóa optional trong Swift. Tại sao cần sử dụng nó?

Optional là một loại kiểu dữ liệu đặc biệt trong Swift, cho phép một biến có thể chứa giá trị hoặc không có giá trị (nil). Điều này giúp lập trình viên xử lý các trường hợp mà một giá trị có thể không tồn tại mà không gặp phải lỗi trong quá trình thực thi.

Để khai báo một biến là optional, bạn sử dụng dấu hỏi (?) sau kiểu dữ liệu:

var name: String? // name có thể là String hoặc nil

Lợi ích của việc sử dụng Optional

  1. An toàn với nil: Optional giúp tránh các lỗi runtime khi cố gắng truy cập vào giá trị nil, một vấn đề phổ biến trong các ngôn ngữ lập trình khác. Thay vì xảy ra lỗi, bạn có thể kiểm tra sự tồn tại của giá trị trước khi sử dụng nó.
  2. Rõ ràng trong thiết kế: Việc sử dụng optional làm cho mã nguồn rõ ràng hơn về mặt ý nghĩa. Khi một biến được khai báo là optional, nó thông báo cho lập trình viên rằng giá trị có thể không tồn tại, từ đó yêu cầu phải kiểm tra trước khi sử dụng.
  3. Hỗ trợ unwrapping: Swift cung cấp các kỹ thuật unwrapping an toàn để truy cập vào giá trị của optional, như if let hoặc guard let. Điều này giúp dễ dàng quản lý các tình huống có thể xảy ra với nil mà không làm tăng độ phức tạp.
  4. Hỗ trợ chaining: Bạn có thể sử dụng optional chaining để gọi phương thức hoặc thuộc tính của một optional mà không cần kiểm tra nil mỗi lần.

Khi nào cần sử dụng Optional?

  • Khi bạn cần một biến có thể không có giá trị hợp lệ.
  • Khi bạn làm việc với dữ liệu không xác định, chẳng hạn như dữ liệu từ API, nơi một số trường có thể không có giá trị.
  • Khi bạn muốn biểu thị rõ ràng rằng một giá trị có thể tồn tại hoặc không tồn tại, nhằm nâng cao độ an toàn và rõ ràng của mã nguồn.

Tìm hiểu thêm tại: https://developer.apple.com/documentation/swift/optional

guard và if let trong Swift khác nhau như thế nào? Khi nào nên sử dụng mỗi loại?

Đặc điểm if let  guard let
Cú pháp  Được sử dụng bên trong một khối lệnh, và khi điều kiện thỏa mãn, khối lệnh if sẽ được thực thi. Được sử dụng để kiểm tra điều kiện, và yêu cầu else để thoát ra khỏi phạm vi hiện tại nếu điều kiện không thỏa mãn.
Phạm vi sử dụng Giá trị được unwrapped chỉ tồn tại bên trong khối lệnh if. Giá trị được unwrapped tồn tại bên ngoài khối lệnh guard, trong phạm vi hiện tại.
Ý nghĩa logic Dùng để thực hiện các hành động khác nhau dựa trên việc có giá trị hay không. Dùng để đảm bảo rằng điều kiện được thỏa mãn trước khi tiếp tục thực thi logic phía sau. Nếu không thỏa mãn, thường kết thúc hoặc thoát khỏi hàm.
Tính tường minh Tốt hơn khi xử lý các trường hợp mà cả hai nhánh đều quan trọng (có hoặc không có giá trị). Thường được dùng để giảm độ sâu của mã và làm rõ yêu cầu kiểm tra ngay từ đầu.
  • Sử dụng if let khi:
    • Bạn chỉ muốn xử lý giá trị đã được unwrapped trong một phạm vi nhỏ.
    • Cả hai trường hợp có và không có giá trị đều cần xử lý logic cụ thể.
    • Bạn muốn thực hiện các hành động khác nhau dựa trên việc có giá trị hay không.
  • Sử dụng guard let khi:
    • Bạn muốn đảm bảo rằng giá trị đã được unwrapped và tiếp tục xử lý phần logic phía sau.
    • Bạn muốn giảm độ sâu của mã nguồn (tránh viết quá nhiều khối if lồng nhau).
    • Bạn cần dừng hoặc thoát khỏi một hàm sớm nếu một giá trị không tồn tại (nil).

Tóm lại:

  • guard let giúp mã nguồn dễ đọc hơn khi bạn cần kiểm tra nhiều giá trị
  • if let phù hợp hơn cho các tình huống kiểm tra đơn giản, nơi có nhiều cách xử lý khác nhau dựa trên điều kiện

Hàm bậc cao (higher-order function) trong Swift là gì? Cho một ví dụ như map, filter, hoặc reduce

Hàm bậc cao trong Swift là các hàm có thể nhận các hàm khác làm đối số hoặc trả về một hàm khác. Chúng giúp xử lý các tập hợp dữ liệu và thao tác với các đối tượng theo cách ngắn gọn và trực quan hơn, bằng cách áp dụng một hàm cho các phần tử của một mảng hoặc một tập hợp.

Tìm hiểu thêm về higher-order function

Map

map là một hàm bậc cao được sử dụng để áp dụng một hàm cho mỗi phần tử của một mảng, sau đó trả về một mảng mới chứa kết quả của các phép biến đổi đó.

let numbers = [1, 2, 3, 4, 5]
let squaredNumbers = numbers.map { $0 * $0 }
print(squaredNumbers) // Output: [1, 4, 9, 16, 25]

Trong ví dụ trên, map áp dụng hàm { $0 * $0 } lên mỗi phần tử của mảng numbers, tạo ra một mảng mới squaredNumbers chứa các giá trị bình phương của từng phần tử.

Filer

filter là một hàm bậc cao cho phép bạn lọc các phần tử của một mảng dựa trên một điều kiện. Nó trả về một mảng mới chứa các phần tử đáp ứng điều kiện đã cho.

let numbers = [1, 2, 3, 4, 5]
let evenNumbers = numbers.filter { $0 % 2 == 0 }
print(evenNumbers) // Output: [2, 4]

Ở đây, filter áp dụng điều kiện { $0 % 2 == 0 } để chỉ giữ lại các số chẵn trong mảng numbers, tạo ra mảng mới evenNumbers.

Reduce 

reduce là một hàm bậc cao được sử dụng để tổng hợp các giá trị của một mảng thành một giá trị duy nhất, bằng cách áp dụng một phép toán cho từng phần tử.

let numbers = [1, 2, 3, 4, 5]
let sum = numbers.reduce(®) { $0 + $1 }
print(sum) // Output: 15

reduce bắt đầu với giá trị ban đầu là 0, sau đó cộng dồn từng phần tử của mảng numbers để tính tổng. Biến $0 đại diện cho tổng tạm thời, còn $1 là từng phần tử trong quá trình lặp.

lazy trong Swift là gì và khi nào nên sử dụng?

lazy là một từ khóa trong Swift dùng để khai báo một thuộc tính chỉ được khởi tạo khi nó được truy cập lần đầu tiên. Nó trì hoãn việc khởi tạo cho đến khi cần thiết, giúp tối ưu hóa hiệu suất và tránh việc sử dụng tài nguyên không cần thiết ngay từ đầu.

Sử dụng Lazy cho các trường hợp sau:

  • Khi việc khởi tạo biến tốn nhiều tài nguyên:

Nếu một thuộc tính yêu cầu nhiều thời gian hoặc bộ nhớ để khởi tạo, và không phải lúc nào cũng cần thiết, bạn nên sử dụng lazy. Việc này giúp trì hoãn việc khởi tạo cho đến khi thuộc tính được sử dụng thực sự, từ đó cải thiện hiệu suất.

  • Khi thuộc tính phụ thuộc vào giá trị của các thuộc tính khác:

lazy có thể được sử dụng khi thuộc tính cần truy cập các giá trị khác mà chưa được khởi tạo tại thời điểm đối tượng được tạo ra. Nhờ lazy, thuộc tính này sẽ chỉ được khởi tạo sau khi các thuộc tính khác đã có giá trị.

  • Khi bạn muốn trì hoãn việc khởi tạo đến thời điểm cần thiết:

Nếu thuộc tính không được sử dụng trong mọi trường hợp, việc sử dụng lazy giúp tránh việc tạo ra các đối tượng không cần thiết.

Giải thích sự khác nhau giữa weak và unowned trong Swift

Đặc điểm  weak  unowned
Bản chất  Tham chiếu yếu, không giữ mạnh đối tượng mà nó tham chiếu. Tham chiếu không sở hữu, không giữ mạnh đối tượng mà nó tham chiếu.
Optional Phải khai báo là optional (SomeClass?). Không phải là optional (khai báo trực tiếp kiểu SomeClass).
Khi đối tượng bị hủy Tham chiếu tự động trở thành nil. Gây ra lỗi runtime nếu cố truy cập vào đối tượng đã bị hủy.
Cú pháp weak var someObject: SomeClass? unowned var someObject: SomeClass
Khi nào nên sử dụng Khi đối tượng có thể trở thành nil sau một thời điểm nào đó. Khi chắc chắn rằng đối tượng sẽ không bị hủy trước tham chiếu unowned.
Tránh retain cycle Đúng, thường dùng trong các trường hợp delegate hoặc closure. Đúng, thường dùng khi quan hệ cha-con mà con phụ thuộc vào cha.

Việc chọn giữa weakunowned phụ thuộc vào việc liệu đối tượng có thể trở thành nil hay không và vòng đời của các đối tượng liên quan trong ứng dụng của bạn.

Ví dụ

Weak

class Person {
  var name: String
  init(name: String) {
    self.name = name
  }
}

class Apartment {
  weak var tenant: Person? // Tham chiếu yếu đến một đối tượng `Person`
}

var john: Person? = Person (name: "John")
var apartment: Apartment? = Apartment()

apartment?.tenant = john
john = nil
// Khi 'john` bị gán nil, `apartment?.tenant` cũng trở thành nil

Trong ví dụ trên, tenant là một tham chiếu yếu đến Person. Khi john bị gán nil, tenant trong Apartment cũng tự động trở thành nil.

Unowned

class Customer {
  var name: String
  var card: CreditCard?
  init(name: String) {
    self.name = name
  }
}

class CreditCard {
  unowned var owner: Customer // Tham chiếu không sở hữu đến 'Customer
  init(owner: Customer) {
    self.owner = owner
  }
}

var john: Customer? = Customer (name: "John")
john?.card = CreditCard (owner: john!)

john = nil

Trong ví dụ trên, CreditCard có một tham chiếu unowned đến Customer, giả định rằng Customer sẽ luôn tồn tại cho đến khi CreditCard bị hủy. Khi john bị gán nil, đối tượng CreditCard cũng sẽ bị hủy.

Enum với associated values trong Swift là gì? Cho một ví dụ

Trong Swift, enum với associated values cho phép bạn định nghĩa các trường hợp (cases) của một enum kèm theo các giá trị bổ sung. Điều này giúp mở rộng khả năng của enum, cho phép mỗi trường hợp lưu trữ các giá trị cụ thể liên quan đến nó.

Thay vì chỉ đơn thuần định nghĩa các trường hợp cố định, bạn có thể gán các giá trị đi kèm mỗi lần sử dụng trường hợp đó. Mỗi trường hợp của enum có thể có một hoặc nhiều giá trị với các kiểu dữ liệu khác nhau.

Dưới đây là ví dụ về một enum mô tả trạng thái của một yêu cầu mạng (network request) với các giá trị liên quan:

enum NetworkResult {
    case success(data: Data)
    case failure(error: String)
    case loading(progress: Double)
}

Các trường hợp:

  • success: Trường hợp này có một giá trị đi kèm là data có kiểu Data (dữ liệu tải về thành công).
  • failure: Trường hợp này có một giá trị đi kèm là error kiểu String (thông báo lỗi khi yêu cầu thất bại).
  • loading: Trường hợp này có một giá trị đi kèm là progress kiểu Double (tỷ lệ hoàn thành của yêu cầu).

Cách sử dụng Enum với Associated Values

Dưới đây là cách sử dụng enum với các giá trị đi kèm:

let result1 = NetworkResult.success(data: Data())
let result2 = NetworkResult.failure(error: "Network error")
let result3 = NetworkResult.loading(progress: 0.5)

switch result2 {
case .success(let data):
    print("Download succeeded with data: \(data)")
case .failure(let error):
    print("Download failed with error: \(error)")
case .loading(let progress):
    print("Download in progress: \(progress * 100)%")
}

Giải thích:

  • Khởi tạo giá trị: Khi tạo các giá trị result1, result2, result3, mỗi trường hợp có thể đi kèm các giá trị tương ứng.
  • Sử dụng switch: Khi sử dụng switch, bạn có thể truy xuất các giá trị đi kèm bằng cách sử dụng từ khóa let để nhận giá trị từ từng trường hợp.

Tìm hiểu thêm tại: https://docs.swift.org/swift-book/documentation/the-swift-programming-language/enumerations/

Codable trong Swift là gì? Làm thế nào để sử dụng nó để mã hóa và giải mã JSON?

Codable là một protocol trong Swift giúp mã hóa (encoding) và giải mã (decoding) dữ liệu giữa các đối tượng Swift và các định dạng dữ liệu như JSON. Codable kết hợp hai protocol EncodableDecodable, giúp dễ dàng chuyển đổi qua lại giữa các kiểu dữ liệu và JSON, rất hữu ích khi làm việc với API.

Để sử dụng Codable, cần tạo một struct hoặc class tuân thủ Codable. Ví dụ, với một struct User:

struct User: Codable {
  var id: Int
  var name: String
  var email: String
}

Giải mã JSON thành đối tượng:

let jsonString = """
{
  "id": 1,
  "name": "John Doe",
  "email": "john@example.com"
}
"""

if let jsonData = jsonString.data(using: .utf8) {
  let user = try? JSONDecoder().decode(User.self, from: jsonData)
}

Mã hóa đối tượng thành JSON:

let user = User(id: 1, name: "John Doe", email: "john@example.com")
let jsonData = try? JSONEncoder().encode(user)
let jsonString = String(data: jsonData!, encoding: .utf8)
  • Giải mã (decode): Sử dụng JSONDecoder để chuyển dữ liệu JSON thành đối tượng Swift.
  • Mã hóa (encode): Sử dụng JSONEncoder để chuyển đối tượng Swift thành JSON.

Generics trong Swift là gì? Lợi ích của việc sử dụng Generics?

Generics trong Swift cho phép bạn viết các hàm, lớp, struct, hoặc enum có thể làm việc với bất kỳ kiểu dữ liệu nào, thay vì chỉ giới hạn ở một kiểu cụ thể. Điều này giúp bạn viết code tổng quát hơn, tái sử dụng được với nhiều loại dữ liệu khác nhau.

Ví dụ, một hàm thông thường để hoán đổi hai số nguyên có thể được viết như sau:

func swapTwoValues<T>(__ a: inout T, b: inout T) {
  let temp = a
  a = b
  b = temp
}

Hàm này chỉ có thể hoán đổi Int. Với Generics, có thể viết một hàm hoán đổi có thể làm việc với bất kỳ kiểu dữ liệu nào

Ở đây, T là một placeholder cho kiểu dữ liệu, và Swift sẽ tự động xác định kiểu dữ liệu khi sử dụng hàm. Bạn có thể gọi swapTwoValues với Int, String, hoặc bất kỳ kiểu nào khác.

Lợi ích của việc sử dụng Generics:

  1. Tái sử dụng mã (Code Reusability): Giúp viết các hàm và kiểu dữ liệu có thể sử dụng cho nhiều kiểu khác nhau, tránh phải viết lại cùng một logic cho từng kiểu.
  2. An toàn về kiểu (Type Safety): Cho phép kiểm tra lỗi liên quan đến kiểu dữ liệu ngay tại thời điểm biên dịch, giúp tránh lỗi runtime.
  3. Dễ bảo trì (Maintainability): Với các đoạn mã tổng quát và đơn giản hơn, bạn có thể dễ dàng bảo trì và mở rộng code.

Các câu hỏi phỏng vấn iOS Developer liên quan đến iOS SDK

Giải thích vòng đời của một UIViewController. Các phương thức nào được gọi khi view xuất hiện hoặc biến mất?

Vòng đời của UIViewController bao gồm các phương thức quản lý các giai đoạn khi view được tải, hiển thị, ẩn, hoặc hủy. Dưới đây là các phương thức chính trong vòng đời của một UIViewController:

  1. viewDidLoad(): Được gọi một lần khi view controller tải xong view của nó vào bộ nhớ. Đây là nơi để cấu hình các thành phần UI và tải dữ liệu ban đầu.
  2. viewWillAppear(_:): Được gọi ngay trước khi view sắp xuất hiện trên màn hình. Thường dùng để cập nhật UI theo trạng thái mới nhất hoặc bắt đầu các hoạt động cần thiết khi view hiển thị (ví dụ: kích hoạt các thay đổi dữ liệu từ model).
  3. viewDidAppear(_:): Được gọi ngay sau khi view đã xuất hiện trên màn hình. Đây là nơi bắt đầu các tác vụ như animation, lấy dữ liệu theo thời gian thực, hoặc khởi động các timer.
  4. viewWillDisappear(_:): Được gọi ngay trước khi view sắp bị ẩn khỏi màn hình. Thường dùng để lưu dữ liệu, hủy các tác vụ không cần thiết, hoặc dừng các hiệu ứng động.
  5. viewDidDisappear(_:): Được gọi ngay sau khi view đã biến mất khỏi màn hình. Đây là nơi giải phóng tài nguyên hoặc dừng các hoạt động không cần thiết để tiết kiệm bộ nhớ.

Cách quản lý bộ nhớ trong iOS là gì? Giải thích ARC (Automatic Reference Counting)

iOS sử dụng ARC (Automatic Reference Counting) để quản lý bộ nhớ tự động cho các đối tượng. ARC giúp theo dõi và quản lý số lượng tham chiếu đến từng đối tượng trong ứng dụng và sẽ giải phóng bộ nhớ của đối tượng khi không còn tham chiếu nào đến nó. ARC giúp đảm bảo ứng dụng sử dụng bộ nhớ hiệu quả mà không cần giải phóng bộ nhớ thủ công.

Cách thức hoạt động của ARC:

  • Strong Reference: Mỗi khi một biến hoặc đối tượng có tham chiếu mạnh (strong) đến một đối tượng, bộ đếm tham chiếu của đối tượng đó tăng lên.
  • Deallocation: Khi bộ đếm tham chiếu của đối tượng giảm về 0 (không còn tham chiếu nào), ARC sẽ tự động giải phóng bộ nhớ của đối tượng đó.

Các loại tham chiếu trong ARC:

  1. Strong Reference: Giữ đối tượng trong bộ nhớ cho đến khi không còn tham chiếu nào.
  2. Weak Reference: Không giữ đối tượng trong bộ nhớ; khi không còn strong reference nào, đối tượng sẽ được giải phóng.
  3. Unowned Reference: Tương tự weak, nhưng chỉ được dùng khi biết chắc chắn rằng đối tượng sẽ luôn tồn tại trong suốt vòng đời của unowned reference (tránh các trường hợp bị gán thành nil).

câu hỏi phỏng vấn ios developer - itviec blog

Trên đây là hình minh hoạ về cơ chế quản lý bộ nhớ trong iOS.

Delegate và Closure trong Swift khác nhau như thế nào? Khi nào nên sử dụng từng loại?

Thuộc tính  Delegate Closure
Định nghĩa  Một thiết kế hướng đối tượng, cho phép một đối tượng giao nhiệm vụ cho đối tượng khác. Khối mã không có tên, có thể lưu trữ và truyền qua các hàm như một tham số.
Cách sử dụng Thường là một protocol có các phương thức được gọi bởi đối tượng khác để phản hồi các sự kiện hoặc tương tác. Được truyền trực tiếp để xử lý các tác vụ đơn giản, trả về kết quả hoặc thực hiện hành động ngắn gọn.
Tính mở rộng Tạo mối quan hệ rõ ràng và lâu dài giữa hai đối tượng, thích hợp cho các giao diện phức tạp hoặc các lớp cần giao tiếp hai chiều. Phù hợp với các tác vụ ngắn gọn, không yêu cầu mối quan hệ lâu dài giữa các đối tượng.

Khi nào sử dụng từng loại:

  • Delegate: Sử dụng khi cần giao tiếp phức tạp và liên tục giữa hai đối tượng, đặc biệt khi một đối tượng cần phản hồi nhiều sự kiện khác nhau. Thích hợp cho các trường hợp như điều khiển UI, nơi một controller cần nhận thông báo từ view (như UITableViewDelegate).
  • Closure: Sử dụng cho các tác vụ ngắn hạn hoặc đơn giản, nơi bạn cần truyền một khối mã để thực hiện một lần, chẳng hạn như xử lý kết quả từ một API call, animation completion, hoặc xử lý tác vụ không đồng bộ.

NotificationCenter là gì và khi nào nên sử dụng?

NotificationCenter trong iOS là một cơ chế cung cấp cách thức để truyền thông tin giữa các đối tượng mà không cần tạo mối liên kết trực tiếp giữa chúng. Với NotificationCenter, một đối tượng có thể “post” (gửi) một thông báo, và các đối tượng khác có thể “observe” (quan sát) và phản hồi khi thông báo đó được phát đi. Điều này giúp giảm phụ thuộc giữa các đối tượng, tăng tính linh hoạt và khả năng mở rộng của ứng dụng.

Cách sử dụng:

  • Đăng ký một đối tượng để quan sát một thông báo: Sử dụng phương thức addObserver(_:selector:name:object:).
  • Gửi (post) một thông báo: Sử dụng post(name:object:userInfo:) để phát đi thông báo.

Khi nào nên sử dụng NotificationCenter:

  1. Truyền thông giữa các thành phần không có mối liên kết trực tiếp: Khi cần truyền dữ liệu hoặc thông báo sự kiện giữa các lớp hoặc module không có mối quan hệ trực tiếp.
  2. Cập nhật nhiều đối tượng khi có sự kiện xảy ra: Khi một sự kiện (như thay đổi dữ liệu, thay đổi trạng thái mạng) xảy ra và cần thông báo đến nhiều đối tượng cùng lúc mà không cần phải giữ tham chiếu đến từng đối tượng đó.
  3. Giảm phụ thuộc (Loosely Coupling): Sử dụng NotificationCenter giúp giảm độ phụ thuộc giữa các lớp, cải thiện khả năng bảo trì và tái sử dụng mã.

Ví dụ sử dụng Notification Center:

import UIKit

class LoginViewController: UIViewController {
    func loginUser() {
        // Giả sử người dùng đã đăng nhập thành công
        print("User logged in successfully")

        // Gửi thông báo "UserDidLogin"
        NotificationCenter.default.post(name: Notification.Name("UserDidLogin"), object: nil)
    }
}

Trong lớp LoginViewController, sau khi người dùng đăng nhập thành công, gửi thông báo bằng cách dùng NotificationCenter.

import UIKit

class HomeViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        // Đăng ký lắng nghe thông báo "UserDidLogin"
        NotificationCenter.default.addObserver(self, selector: #selector(userDidLogin), name: Notification.Name("UserDidLogin"), object: nil)
    }

    // Phương thức sẽ được gọi khi nhận thông báo
    @objc func userDidLogin() {
        print("Notification received: User logged in")
        // Cập nhật giao diện, tải dữ liệu, hoặc thực hiện các hành động cần thiết
    }

    deinit {
        NotificationCenter.default.removeObserver(self, name: Notification.Name("UserDidLogin"), object: nil)
    }
}

Giải thích:

  • Đăng thông báo: Sau khi đăng nhập, LoginViewController gửi thông báo “UserDidLogin” để báo cho các lớp khác rằng quá trình đăng nhập đã hoàn tất.
  • Lắng nghe thông báo: HomeViewController đăng ký để lắng nghe thông báo “UserDidLogin” và gọi phương thức userDidLogin khi nhận được thông báo, giúp nó thực hiện các hành động liên quan, chẳng hạn như cập nhật UI.

Phân biệt giữa frame và bounds của một UIView

Thuộc tính  Frame Bounds
Định nghĩa  Xác định vị trí và kích thước của UIView trong hệ tọa độ của superview (view cha). Xác định vị trí và kích thước của UIView trong chính hệ tọa độ của nó.
Hệ toạ độ  Hệ tọa độ của superview (view cha). Hệ tọa độ nội bộ của UIView (0,0 ở góc trên bên trái).
Thay đổi vị trí Thay đổi frame.origin sẽ di chuyển UIView trong superview. Thay đổi bounds.origin sẽ di chuyển nội dung bên trong UIView mà không ảnh hưởng đến vị trí của nó trong superview.
Ứng dụng chính Dùng để định vị UIView trong superview. Dùng để cuộn hoặc cắt nội dung của chính UIView từ bên trong.

câu hỏi phỏng vấn ios developer - itviec blog

Hình ảnh trên miêu tả cách sử dụng khác nhau trong giao diện di động của Frame và Bounds.

Core Data là gì? Khi nào nên sử dụng Core Data so với các tùy chọn lưu trữ dữ liệu khác?

Core Data là một framework quản lý dữ liệu trong iOS được Apple cung cấp, cho phép lưu trữ, truy vấn và quản lý dữ liệu cục bộ dưới dạng các đối tượng. Core Data hỗ trợ mô hình hóa dữ liệu quan hệ và đi kèm với các tính năng như tự động lưu trữ, truy vấn, và tối ưu hóa bộ nhớ.

Khi nào nên sử dụng Core Data:

  • Khi cần quản lý dữ liệu phức tạp: Core Data rất hữu ích cho các ứng dụng cần lưu trữ và xử lý lượng lớn dữ liệu có quan hệ phức tạp giữa các thực thể, chẳng hạn như ứng dụng quản lý ghi chú, lịch, hay quản lý tài chính.
  • Khi cần hiệu suất cao: Core Data tối ưu hóa việc truy vấn và quản lý bộ nhớ thông qua tính năng lazy loading và caching, giúp hiệu suất ứng dụng ổn định ngay cả với lượng dữ liệu lớn.
  • Khi cần hỗ trợ undo/redo: Core Data có sẵn tính năng undo/redo cho phép người dùng quay lại hoặc lặp lại các thay đổi dễ dàng.
  • Khi cần theo dõi và lưu trữ trạng thái: Core Data hỗ trợ theo dõi trạng thái của các đối tượng, rất phù hợp cho các ứng dụng yêu cầu theo dõi sự thay đổi và đồng bộ dữ liệu liên tục.

So sánh với các tùy chọn lưu trữ khác:

Tùy chọn lưu trữ Tình huống sử dụng
Core Data Khi cần quản lý dữ liệu phức tạp, tối ưu hóa truy vấn, và có mối quan hệ giữa các đối tượng.
UserDefaults Khi lưu trữ dữ liệu nhỏ, không phức tạp, chẳng hạn như cài đặt, tùy chọn người dùng.
Keychain Khi cần lưu trữ dữ liệu bảo mật như token, mật khẩu.
File Storage Khi lưu trữ tệp tin, hình ảnh hoặc dữ liệu không quan hệ (không cần truy vấn).
SQLite Khi cần cơ sở dữ liệu quan hệ đơn giản mà không cần tính năng quản lý đối tượng của Core Data.

URLSession là gì và làm thế nào để thực hiện một yêu cầu mạng?

URLSession là một API trong Swift cho phép ứng dụng giao tiếp với mạng, thực hiện các tác vụ như tải dữ liệu, tải file, hoặc tải lên dữ liệu. URLSession cung cấp các phương thức đồng bộ và không đồng bộ để thực hiện các yêu cầu HTTP hoặc HTTPS.

Cách thực hiện một yêu cầu mạng với URLSession:

  1. Tạo URL: Xác định URL của yêu cầu.
  2. Tạo URLSession: Sử dụng URLSession.shared cho các tác vụ đơn giản hoặc cấu hình riêng bằng cách tạo URLSessionConfiguration.
  3. Tạo yêu cầu (Request): Tạo một yêu cầu với phương thức và các tham số cần thiết.
  4. Tạo task: Sử dụng dataTask để gửi yêu cầu và xử lý phản hồi.
  5. Thực thi task: Gọi task.resume() để bắt đầu yêu cầu.

Ví dụ:

import Foundation

func fetchData() {
    // 1. Tạo URL
    guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else { return }

    // 2. Tạo URLSession
    let session = URLSession.shared

    // 3. Tạo task
    let task = session.dataTask(with: url) { data, response, error in
        // Kiểm tra lỗi
        if let error = error {
            print("Error: \(error.localizedDescription)")
            return
        }

        // Kiểm tra dữ liệu và phản hồi
        guard let data = data, let response = response as? HTTPURLResponse, response.statusCode == 200 else {
            print("No data or status code error")
            return
        }

        // Xử lý dữ liệu JSON
        do {
            if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [[String: Any]] {
                print("Received JSON: \(json)")
            }
        } catch {
            print("JSON decoding error: \(error.localizedDescription)")
        }
    }

    // 4. Thực thi task
    task.resume()
}

Giải thích:

  • URLSession.shared: Tạo một phiên mặc định cho các tác vụ đơn giản.
  • dataTask(with:): Tạo một data task để thực hiện yêu cầu GET.
  • task.resume(): Bắt đầu thực thi yêu cầu.
  • Xử lý JSON: Sử dụng JSONSerialization để giải mã dữ liệu JSON.

GCD (Grand Central Dispatch) là gì? Cách sử dụng DispatchQueue để thực hiện công việc bất đồng bộ?

GCD (Grand Central Dispatch) là một framework trong iOS và macOS giúp quản lý các tác vụ trên nhiều luồng một cách hiệu quả, cho phép thực hiện các công việc bất đồng bộ và tối ưu hóa tài nguyên hệ thống. Với GCD, bạn có thể tránh khóa giao diện người dùng khi xử lý các tác vụ nặng như tải dữ liệu từ mạng hoặc xử lý ảnh.

Cách sử dụng DispatchQueue để thực hiện công việc bất đồng bộ:

  • DispatchQueue là một phần của GCD cho phép bạn xác định hàng đợi tác vụ và kiểm soát khi nào và ở đâu các tác vụ này sẽ được thực hiện.
  • Có hai loại hàng đợi phổ biến:
    • Main Queue: Thực hiện tác vụ trên luồng chính (UI).
    • Global Queue: Thực hiện các tác vụ trên các luồng nền (background).

Dưới đây là ví dụ sử dụng DispatchQueue để thực hiện công việc bất đồng bộ:

import Foundation

func fetchDataInBackground() {
    // Sử dụng Global Queue để tải dữ liệu không đồng bộ
    DispatchQueue.global(qos: .background).async {
        // Giả lập tác vụ nặng như tải dữ liệu
        print("Bắt đầu tải dữ liệu trong nền")

        // Giả lập thời gian xử lý
        sleep(2)  // Ví dụ, chờ 2 giây

        print("Dữ liệu tải xong")

        // Chuyển sang Main Queue để cập nhật giao diện
        DispatchQueue.main.async {
            print("Cập nhật giao diện người dùng trên luồng chính")
        }
    }
}

fetchDataInBackground()

Giải thích:

  1. DispatchQueue.global(qos:): Tạo một hàng đợi nền với mức ưu tiên (qos – quality of service) là .background để thực hiện các tác vụ không ảnh hưởng đến giao diện.
  2. async: Chạy tác vụ một cách bất đồng bộ, tránh làm khóa giao diện người dùng.
  3. DispatchQueue.main.async: Sau khi hoàn thành tác vụ nền, sử dụng DispatchQueue.main.async để chuyển về luồng chính, cập nhật giao diện người dùng an toàn.

Khi nào nên sử dụng GCD với DispatchQueue:

  • Khi cần thực hiện các tác vụ nặng (tải dữ liệu, xử lý ảnh) mà không ảnh hưởng đến trải nghiệm người dùng.
  • Khi cần cập nhật giao diện hoặc kết quả tính toán sau khi hoàn tất tác vụ nền.

Sự khác nhau giữa Main Thread và Background Thread trong iOS là gì? Tại sao cần phân biệt chúng?

Thuộc tính  Main Thread  Backgroud Thread
Định nghĩa Là luồng chính, nơi thực hiện tất cả tác vụ liên quan đến giao diện người dùng (UI). Là luồng nền, thực hiện các tác vụ không yêu cầu cập nhật trực tiếp lên UI.
Nhiệm vụ chính Cập nhật giao diện, xử lý các tương tác người dùng như chạm, cuộn, và thao tác UI. Xử lý tác vụ nặng (tải dữ liệu từ mạng, xử lý ảnh, tính toán) mà không làm ảnh hưởng đến giao diện.
Yêu cầu chính Phải chạy trên Main Thread khi cập nhật UI (thực hiện các thao tác với UIView). Thích hợp cho các tác vụ cần nhiều thời gian xử lý để không làm chậm giao diện.

Auto Layout là gì? Làm thế nào để tạo một giao diện tương thích với nhiều kích thước màn hình?

Auto Layout là một hệ thống trong iOS được sử dụng để xác định vị trí và kích thước của các thành phần giao diện người dùng (UI) một cách linh hoạt và động, dựa trên các ràng buộc (constraints). Hệ thống này giúp tự động điều chỉnh bố cục của ứng dụng cho phù hợp với các kích thước màn hình khác nhau, hướng màn hình (portrait hoặc landscape), và các thiết bị khác nhau (iPhone, iPad).

Cách tạo giao diện tương thích với nhiều kích thước màn hình bằng Auto Layout:

  1. Sử dụng Constraints:
    • Thiết lập các ràng buộc giữa các thành phần UI để xác định cách mà chúng sẽ di chuyển hoặc thay đổi kích thước khi kích thước màn hình thay đổi.
    • Ví dụ: bạn có thể đặt một nút cách cạnh trên 20 điểm và cách cạnh trái 15 điểm. Điều này đảm bảo rằng nút sẽ giữ khoảng cách nhất định với các cạnh của màn hình.
  2. Hệ thống Size Classes:
    • Sử dụng Size Classes để xác định cách bố trí cho các thiết bị và hướng màn hình khác nhau.
    • Bạn có thể thiết lập các giao diện khác nhau cho các size class khác nhau, ví dụ như compact width cho iPhone và regular width cho iPad.
  3. Bố cục tương thích (Adaptive Layout):
    • Tạo các bố cục linh hoạt sử dụng các View và Stack Views.
    • Stack Views giúp bạn dễ dàng sắp xếp các thành phần UI theo chiều ngang hoặc chiều dọc mà không cần thiết lập nhiều ràng buộc.
  4. Chạy thử trên nhiều thiết bị:

Sử dụng Simulator hoặc các thiết bị thật để kiểm tra giao diện trên nhiều kích thước màn hình khác nhau, đảm bảo rằng giao diện hiển thị đẹp mắt và dễ sử dụng trên mọi thiết bị.

Các câu hỏi phỏng vấn iOS Developer liên quan đến UI/UX & Kiến trúc

Giải thích MVVM là gì? Nó khác gì so với MVC trong iOS?

MVVM (Model-View-ViewModel):

MVVM là một mẫu kiến trúc phần mềm được thiết kế để hỗ trợ phát triển ứng dụng với giao diện người dùng phong phú. Nó giúp tách biệt logic điều khiển (business logic) khỏi giao diện người dùng, tạo điều kiện cho việc phát triển và bảo trì dễ dàng hơn.

  • Model: Đại diện cho dữ liệu và các quy tắc kinh doanh. Model không biết gì về View hoặc ViewModel.
  • View: Là giao diện người dùng (UI) mà người dùng tương tác. View chỉ nhận thông tin từ ViewModel và không xử lý logic kinh doanh.
  • ViewModel: Là cầu nối giữa Model và View. Nó chứa logic điều khiển và các thuộc tính cần thiết để hiển thị trên View. ViewModel có thể có các thuộc tính thông báo cho View về những thay đổi cần được cập nhật.

MVC (Model-View-Controller):

MVC là một mẫu kiến trúc phổ biến trong phát triển ứng dụng, bao gồm ba thành phần chính:

  • Model: Tương tự như trong MVVM, Model chứa dữ liệu và logic kinh doanh.
  • View: Giao diện người dùng mà người dùng tương tác.
  • Controller: Là cầu nối giữa Model và View. Controller nhận các sự kiện từ View, cập nhật Model và sau đó cập nhật View.

Combine trong iOS là gì? Khi nào nên sử dụng?

Combine là một framework được Apple giới thiệu trong iOS 13, cho phép bạn xây dựng các ứng dụng sử dụng lập trình phản ứng. Nó cung cấp các công cụ để xử lý các sự kiện bất đồng bộ và quản lý dữ liệu theo cách đơn giản và dễ hiểu.

Tính năng chính của Combine:

  • Publishers and Subscribers: Combine hoạt động dựa trên mô hình nhà xuất bản (publisher) và người đăng ký (subscriber). Publishers phát hành các giá trị theo thời gian và Subscribers nhận các giá trị đó.
  • Operators: Combine cung cấp một loạt các toán tử (operators) để biến đổi và kết hợp các giá trị, cho phép bạn thực hiện các thao tác phức tạp trên dữ liệu một cách dễ dàng.
  • Chaining: Bạn có thể kết hợp nhiều toán tử để tạo ra một chuỗi xử lý dữ liệu, giúp tăng tính dễ đọc và bảo trì của mã.

Khi nào nên sử dụng Combine:

  1. Xử lý dữ liệu bất đồng bộ: Khi bạn cần theo dõi và phản ứng với các thay đổi trong dữ liệu, chẳng hạn như từ mạng, cơ sở dữ liệu hoặc giao diện người dùng.
  2. Tương tác với UI: Khi bạn muốn cập nhật giao diện người dùng dựa trên các sự kiện khác nhau, như nhấn nút, nhập dữ liệu, hoặc thay đổi trong mô hình.
  3. Xử lý chuỗi dữ liệu phức tạp: Khi bạn cần thực hiện các thao tác như lọc, ánh xạ, và kết hợp nhiều nguồn dữ liệu mà không làm rối mã.

câu hỏi phỏng vấn ios developer - itviec blog

Tại sao SwiftUI dùng Struct cho View chứ không phải Class?

SwiftUI sử dụng Struct cho các View vì một số lý do chính:

  1. Hiệu suất và Tối ưu hóa Bộ nhớ: Struct là một kiểu dữ liệu value type, nghĩa là mỗi khi một Struct được tạo mới hoặc thay đổi, nó sẽ tạo một bản sao độc lập. Điều này giúp SwiftUI dễ dàng tạo ra các View mới khi dữ liệu thay đổi, giảm thiểu tác động đến bộ nhớ so với Class, vốn là reference type và thường phức tạp hơn khi cần quản lý vòng đời.
  2. Immutable và Predictable: Trong SwiftUI, Struct giúp giữ các View bất biến (immutable), nghĩa là trạng thái của một View sẽ không thay đổi sau khi được khởi tạo. Điều này giúp đảm bảo rằng View sẽ không gây ra các trạng thái khó lường và dễ dàng tối ưu hóa. SwiftUI có thể dự đoán chính xác các thay đổi trong giao diện dựa trên dữ liệu và trạng thái của Struct, từ đó giảm thiểu các lỗi không mong muốn.
  3. Dễ Quản lý và Update: Với Struct, SwiftUI có thể so sánh nhanh chóng giữa các phiên bản của View khi cần cập nhật giao diện. Nếu có sự thay đổi, SwiftUI chỉ cần cập nhật phần bị ảnh hưởng thay vì phải cập nhật toàn bộ. Điều này giúp giao diện mượt mà và tăng hiệu suất đáng kể.
  4. Concurrency: Struct được quản lý dễ dàng trong lập trình đa luồng. Với Struct, SwiftUI có thể sao chép dữ liệu của View mà không sợ gặp phải vấn đề khi truy cập bộ nhớ giữa các luồng, giúp xử lý đồng thời (concurrency) an toàn và hiệu quả hơn.

Làm thế nào để sử dụng Animation trong SwiftUI? Cho một ví dụ

SwiftUI cung cấp một API đơn giản để thêm hiệu ứng chuyển động vào các View. Bạn có thể sử dụng modifier .animation() để áp dụng hiệu ứng khi có sự thay đổi trạng thái, hoặc sử dụng các phương thức .withAnimation() để thực hiện chuyển động trong quá trình thay đổi trạng thái.

Ví dụ cơ bản về Animation trong SwiftUI

Trong ví dụ này, khi nhấn vào nút, hình tròn sẽ thay đổi kích thước một cách mượt mà bằng cách sử dụng withAnimation.

import SwiftUI

struct ContentView: View {
    @State private var isExpanded = false

    var body: some View {
        VStack {
            Circle()
                .frame(width: isExpanded ? 200 : 100, height: isExpanded ? 200 : 100)
                .foregroundColor(.blue)
                .animation(.easeInOut(duration: 0.5)) // Thêm animation vào trạng thái

            Button("Animate Circle") {
                withAnimation {
                    isExpanded.toggle() // Thay đổi trạng thái để kích hoạt animation
                }
            }
            .padding()
        }
    }
}

Giải thích

  • @State: Biến isExpanded là một biến trạng thái dùng để kiểm soát kích thước của hình tròn.
  • withAnimation: Khi nhấn vào nút, isExpanded thay đổi, và nhờ withAnimation, SwiftUI tự động cập nhật View với hiệu ứng chuyển động.
  • .animation(.easeInOut(duration: 0.5)): Tạo animation với kiểu easeInOut và thời gian kéo dài 0.5 giây, giúp hình tròn thay đổi kích thước mượt mà.

Các kiểu Animation phổ biến trong SwiftUI

  • .linear: Tốc độ animation không đổi.
  • .easeIn: Animation bắt đầu chậm rồi tăng tốc.
  • .easeOut: Animation bắt đầu nhanh và kết thúc chậm.
  • .easeInOut: Animation kết hợp cả easeIneaseOut.

Coordinator pattern là gì và tại sao nó được sử dụng trong iOS?

Coordinator pattern là một mẫu thiết kế giúp tách biệt việc điều hướng (navigation) và quản lý luồng giao diện người dùng (UI flow) khỏi các UIViewController. Điều này giúp làm cho mã nguồn dễ bảo trì hơn và giảm bớt độ phức tạp trong các ViewController, vì các ViewController không còn phải chịu trách nhiệm về việc điều hướng hoặc luồng logic.

Lợi ích của Coordinator Pattern:

  1. Phân tách trách nhiệm: Coordinator pattern di chuyển logic điều hướng ra khỏi UIViewController và đặt chúng vào các lớp riêng gọi là Coordinator. Điều này giúp cho mỗi UIViewController chỉ tập trung vào logic riêng của nó thay vì quản lý toàn bộ luồng giao diện.
  2. Tái sử dụng mã dễ dàng: Việc điều hướng giữa các màn hình được xử lý trong các lớp Coordinator, giúp chúng ta tái sử dụng hoặc chia sẻ luồng điều hướng giữa các phần khác nhau của ứng dụng.
  3. Tăng khả năng bảo trì: Vì luồng điều hướng không nằm trong UIViewController, mã nguồn trở nên dễ bảo trì hơn. Các thay đổi về điều hướng có thể được thực hiện trong lớp Coordinator mà không ảnh hưởng đến các ViewController.
  4. Thúc đẩy tính mô-đun: Coordinator pattern giúp dễ dàng mở rộng và thêm tính năng mới mà không làm phức tạp thêm các lớp hiện có.

câu hỏi phỏng vấn ios developer - itviec blog

Giải thích cách làm việc với UITableView và UICollectionView. Khi nào nên sử dụng mỗi loại?

UITableViewUICollectionView là hai thành phần giao diện chính trong iOS để hiển thị dữ liệu dạng danh sách. Chúng đều giúp hiển thị một tập hợp các phần tử, nhưng mỗi loại có ưu điểm riêng và phù hợp cho các tình huống khác nhau.

Bảng dưới đây sẽ phân biệt những điểm khác nhau và khi nào nên dùng chúng cho từng yêu cầu khác nhau

Thuộc tính  UITableView UICollectionView
Bố cục Một cột và cuộn theo chiều dọc Có thể tùy chỉnh theo dạng lưới, chiều dọc hoặc ngang
DataSource/Delegate UITableViewDataSource, UITableViewDelegate UICollectionViewDataSource, UICollectionViewDelegate
Tùy chỉnh layout Chỉ hỗ trợ chiều dọc Linh hoạt với UICollectionViewLayout
Khi nào nên dùng Danh sách tuyến tính, cột đơn Giao diện dạng lưới hoặc yêu cầu bố cục phức tạp

Custom View là gì trong iOS? Khi nào nên tạo một custom view?

Custom View là một thành phần giao diện tự định nghĩa trong iOS, được xây dựng từ các lớp con của UIView để tạo một giao diện người dùng độc đáo, phù hợp với yêu cầu cụ thể của ứng dụng. Việc tạo một Custom View cho phép bạn kết hợp và tổ chức các thành phần UI như UILabel, UIButton, hoặc UIImageView trong một lớp, giúp giảm sự trùng lặp và tăng tính tái sử dụng của mã nguồn.

Khi nào nên tạo một Custom View?

  1. Khi cần sử dụng lại một phần giao diện phức tạp: Nếu một thành phần UI được dùng nhiều lần trong ứng dụng và có cấu trúc phức tạp, tạo một Custom View sẽ giúp bạn tránh việc lặp lại cùng một mã và dễ dàng bảo trì khi có thay đổi.
  2. Khi muốn tạo các thành phần giao diện tùy chỉnh: Nếu ứng dụng yêu cầu các thành phần giao diện độc đáo mà UIKit không cung cấp sẵn (ví dụ: biểu đồ, thẻ thông tin), Custom View là giải pháp phù hợp. Bạn có thể tự do tùy chỉnh giao diện và hành vi của nó theo yêu cầu.
  3. Khi cần đóng gói logic riêng cho một thành phần UI: Custom View cho phép bạn đóng gói cả giao diện lẫn logic xử lý riêng của một thành phần UI vào một lớp duy nhất, giúp mã nguồn dễ đọc và quản lý hơn.
  4. Khi muốn tách biệt logic giao diện: Nếu bạn muốn giữ cho UIViewController đơn giản hơn, việc đưa các thành phần UI phức tạp vào Custom View sẽ giúp tách biệt logic giao diện khỏi các lớp điều khiển, làm cho mã dễ bảo trì và kiểm thử hơn.

Làm thế nào để lưu và tải ảnh lên từ thư viện ảnh (photo library)?

Các bước để lưu và tải ảnh từ thư viện ảnh như sau 

Bước 1: UIImagePickerController (iOS 13 trở xuống):

  • Cấp quyền NSPhotoLibraryUsageDescription trong Info.plist.
  • Sử dụng UIImagePickerController để mở thư viện ảnh, sau đó lấy ảnh qua imagePickerController(_:didFinishPickingMediaWithInfo:).
let imagePicker = UIImagePickerController()
imagePicker.delegate = self
imagePicker.sourceType = .photoLibrary
present (imagePicker, animated: true)

Bước 2: PHPickerViewController (iOS 14 trở lên):

  • Không cần quyền truy cập.
  • Sử dụng PHPickerViewController với PHPickerConfiguration.
var config = PHPickerConfiguration()
config.selectionLimit = 1
let picker = PHPickerViewController(configuration: config)
picker.delegate = self
present (picker, animated: true)

Lưu ảnh vào thư viện:

Dùng UIImageWriteToSavedPhotosAlbum, đảm bảo có NSPhotoLibraryAddUsageDescription trong Info.plist.

UIImageWriteToSavedPhotosAlbum (image, self, nil, nil)

Cách triển khai Dark Mode trong iOS là gì?

Để triển khai Dark Mode trong iOS, có một số cách tiếp cận để làm cho giao diện tự động thay đổi theo chế độ sáng hoặc tối của hệ thống. Trước tiên, ưu tiên sử dụng màu hệ thống như .systemBackground hoặc .label, vì các màu này tự động thích ứng với Dark Mode mà không cần cấu hình phức tạp.

Nếu cần dùng màu tùy chỉnh, tạo các màu trong Asset Catalog. Trong Asset Catalog, có tùy chọn tạo các màu riêng biệt cho cả chế độ sáng và tối. Việc này đảm bảo giao diện sẽ phù hợp với từng chế độ mà không cần chỉnh sửa mã nguồn nhiều.

Đôi khi, cần phát hiện và phản hồi khi chế độ sáng/tối thay đổi trong lúc ứng dụng đang chạy, sử dụng phương thức traitCollectionDidChange(_:). Phương thức này cho phép kiểm tra sự thay đổi của chế độ giao diện và thực hiện các cập nhật bổ sung nếu cần.

Cuối cùng, nếu muốn kiểm tra chế độ hiện tại (sáng hoặc tối), kiểm tra thuộc tính userInterfaceStyle của traitCollection trong UIViewController hoặc UIView.

Làm thế nào để xử lý việc hiển thị nội dung khi thiết bị xoay màn hình?

Khi thiết bị xoay màn hình, để đảm bảo giao diện hiển thị đúng, có một số cách tiếp cận phổ biến. Trước tiên, sử dụng Auto Layout để các thành phần giao diện tự động điều chỉnh vị trí và kích thước theo chế độ xoay mà không cần mã nguồn phức tạp. Auto Layout hỗ trợ rất tốt cho việc này, vì nó cho phép giao diện phản hồi linh hoạt với nhiều kích thước màn hình.

Nếu cần thực hiện tùy chỉnh cụ thể khi xoay màn hình, sử dụng phương thức viewWillTransition(to:with:) trong UIViewController. Phương thức này cung cấp size mới và cho phép tôi điều chỉnh nội dung hoặc bố cục một cách linh hoạt trước khi chuyển đổi hoàn tất.

Ngoài ra, kiểm tra UIDevice.current.orientation để xác định hướng thiết bị hiện tại nếu cần thực hiện các thay đổi chi tiết dựa trên hướng cụ thể (như hiển thị chi tiết hơn ở chế độ ngang).

Các câu hỏi phỏng vấn liên quan đến tối ưu hoá hiệu năng 

Làm thế nào để tối ưu hóa việc cuộn trong UITableView hoặc UICollectionView?

Để tối ưu hóa việc cuộn trong UITableView hoặc UICollectionView, tôi thường áp dụng một số kỹ thuật sau:

  1. Sử dụng Reusable Cells: Đảm bảo sử dụng dequeueReusableCell(withIdentifier:for:) để tái sử dụng các ô. Việc này giúp giảm thiểu việc tạo mới các ô và cải thiện hiệu suất khi cuộn.
  2. Chỉ tải dữ liệu cần thiết: Áp dụng phân trang hoặc tải dữ liệu khi cần thiết để không tải toàn bộ dữ liệu ngay từ đầu, điều này giúp giảm tải cho bộ nhớ.
  3. Giảm thiểu công việc trong cellForRowAt hoặc cellForItemAt: Chỉ thực hiện các thao tác nhẹ nhàng trong các phương thức này. Nếu cần tính toán nặng, tôi sẽ làm điều đó trước và lưu trữ kết quả.
  4. Sử dụng prefetching: Bật tính năng prefetching để tải trước dữ liệu cho các ô mà người dùng có khả năng cuộn đến, điều này giúp cải thiện trải nghiệm cuộn.
  5. Tối ưu hóa hình ảnh: Khi làm việc với hình ảnh, tôi sẽ sử dụng các thư viện như SDWebImage để tải hình ảnh không đồng bộ và cache hình ảnh. Cũng nên đảm bảo kích thước hình ảnh được tối ưu hóa để phù hợp với kích thước của ô.
  6. Tắt các hiệu ứng không cần thiết: Giảm thiểu các hiệu ứng animation phức tạp hoặc không cần thiết khi cuộn để tránh làm chậm hiệu suất.

Bằng cách áp dụng những kỹ thuật này, tôi có thể cải thiện hiệu suất và trải nghiệm người dùng khi cuộn trong UITableView hoặc UICollectionView.

Instruments là gì và làm thế nào để sử dụng nó để tìm ra các vấn đề về hiệu suất trong ứng dụng iOS?

Instruments là một công cụ mạnh mẽ trong bộ công cụ Xcode, cho phép lập trình viên theo dõi và phân tích hiệu suất của ứng dụng iOS. Instruments giúp phát hiện các vấn đề như rò rỉ bộ nhớ, thời gian thực thi của mã, và các vấn đề liên quan đến CPU, GPU và mạng.

Để sử dụng Instruments, tôi thực hiện theo các bước sau:

  1. Mở Instruments: Từ Xcode, chọn Product -> Profile hoặc nhấn Command + I. Xcode sẽ biên dịch ứng dụng và mở Instruments.
  2. Chọn Template: Chọn một template phù hợp với vấn đề bạn muốn phân tích, ví dụ:
    • Time Profiler để theo dõi hiệu suất CPU.
    • Allocations để phân tích việc cấp phát bộ nhớ.
    • Leaks để tìm các rò rỉ bộ nhớ.
  3. Ghi lại hoạt động: Nhấn nút ghi để bắt đầu theo dõi. Thực hiện các thao tác trong ứng dụng mà bạn nghi ngờ có vấn đề về hiệu suất.
  4. Phân tích dữ liệu: Sau khi dừng ghi, tôi sẽ phân tích các dữ liệu đã thu thập:
    • Time Profiler cho thấy các phương thức nào tiêu tốn nhiều thời gian CPU.
    • Allocations cho biết các đối tượng nào đang chiếm nhiều bộ nhớ.
    • Leaks chỉ ra các rò rỉ bộ nhớ nếu có.
  5. Tối ưu hóa: Dựa trên thông tin từ Instruments, tôi sẽ xác định các điểm cần tối ưu hóa và tiến hành cải thiện mã nguồn hoặc các cấu trúc dữ liệu để nâng cao hiệu suất.

Sử dụng Instruments một cách hợp lý giúp tôi theo dõi và tối ưu hóa hiệu suất ứng dụng một cách hiệu quả.

@escaping là gì trong Swift? Khi nào nên sử dụng?

Trong Swift, từ khóa @escaping được sử dụng để chỉ định rằng một closure có thể được gọi sau khi hàm mà nó được truyền vào đã hoàn thành. Điều này có nghĩa là closure đó “thoát” khỏi vòng đời của hàm.

Khi bạn định nghĩa một closure là @escaping, bạn cho Swift biết rằng closure này có thể được lưu trữ và gọi sau, chẳng hạn như trong các callback hoặc khi xử lý bất đồng bộ.

Khi nào nên sử dụng @escaping:

  • Callback trong hàm bất đồng bộ: Khi bạn truyền một closure vào một hàm thực hiện các tác vụ bất đồng bộ, như gọi API hoặc thực hiện hoạt động nền, closure này cần được đánh dấu là @escaping vì nó sẽ được gọi sau khi hàm hoàn tất.
func fetchData(completion: @escaping (Data?) -> Void) {
  // Thực hiện gọi API bất đồng bộ DispatchQueue.global().async {
  DispatchQueue.global().async {
    let data = ... // tải dữ liệu
    completion(data) // gọi closure sau khi hoàn thành
  }
}
  • Lưu closure trong thuộc tính: Nếu bạn muốn lưu closure vào thuộc tính của một lớp hoặc struct, bạn cần đánh dấu closure đó là @escaping vì nó sẽ được gọi sau khi hàm hoàn tất.
class MyClass{
  var completionHandler: ((String) -> Void)?

  func doSomething (completion: @escaping (String) -> Void) {
    self.completionHandler = completion // lưu closure
  }
}

Làm thế nào để giảm thiểu việc sử dụng bộ nhớ khi xử lý hình ảnh lớn trong iOS?

Để giảm thiểu việc sử dụng bộ nhớ khi xử lý hình ảnh lớn trong iOS, tôi thường áp dụng các phương pháp sau:

  1. Sử dụng hình ảnh có độ phân giải thấp hơn: Khi tải hình ảnh, tôi sẽ sử dụng phiên bản hình ảnh có độ phân giải thấp hơn thay vì hình ảnh gốc. Điều này giúp giảm kích thước bộ nhớ khi hình ảnh được hiển thị.
  2. Lazy Loading: Tải hình ảnh khi cần thiết thay vì tải tất cả hình ảnh cùng lúc. Tôi có thể sử dụng UIImageView với phương thức tải hình ảnh không đồng bộ để tránh làm đông đúc bộ nhớ.
  3. Sử dụng Cache: Sử dụng bộ nhớ đệm để lưu trữ hình ảnh đã tải, như NSCache hoặc thư viện bên ngoài như SDWebImage. Điều này giúp tránh việc tải lại hình ảnh từ nguồn gốc mỗi khi cần hiển thị.
  4. Giảm kích thước hình ảnh khi hiển thị: Trước khi hiển thị hình ảnh, tôi sẽ thay đổi kích thước nó sao cho phù hợp với kích thước của UIImageView hoặc thành phần hiển thị. Việc này có thể được thực hiện thông qua các thư viện như ImageMagick hoặc các phương thức tích hợp sẵn.
  5. Sử dụng CGImage thay vì UIImage: Khi làm việc với hình ảnh lớn, tôi có thể sử dụng CGImage để xử lý trực tiếp trên các hình ảnh mà không cần phải chuyển đổi sang UIImage, điều này có thể tiết kiệm bộ nhớ.
  6. Xử lý hình ảnh trong background: Khi cần xử lý hình ảnh, tôi thường thực hiện nó trong background thread để không làm ảnh hưởng đến giao diện người dùng.

Bằng cách áp dụng những phương pháp này, tôi có thể giảm thiểu việc sử dụng bộ nhớ và cải thiện hiệu suất của ứng dụng khi xử lý hình ảnh lớn.

Phân biệt giữa Synchronous và Asynchronous trong giao tiếp client và server

Synchronous (Đồng bộ)

  • Định nghĩa: Trong giao tiếp đồng bộ, client sẽ gửi yêu cầu đến server và chờ đợi phản hồi trước khi tiếp tục thực hiện bất kỳ thao tác nào khác. Điều này có nghĩa là client sẽ “bị chặn” cho đến khi server trả về kết quả.
  • Ưu điểm:
    • Đơn giản và dễ hiểu, vì luồng thực thi là tuần tự.
    • Phản hồi nhận được ngay lập tức sau khi yêu cầu được gửi.
  • Nhược điểm:
    • Có thể gây ra trải nghiệm người dùng không tốt, vì nếu server phản hồi chậm, client sẽ không thể thực hiện bất kỳ thao tác nào khác trong thời gian chờ đợi.
    • Khó mở rộng, đặc biệt trong trường hợp có nhiều yêu cầu từ nhiều client cùng một lúc.

Asynchronous (Bất đồng bộ)

  • Định nghĩa: Trong giao tiếp bất đồng bộ, client gửi yêu cầu đến server và không cần chờ đợi phản hồi ngay lập tức. Thay vào đó, client có thể tiếp tục thực hiện các tác vụ khác trong khi đang chờ phản hồi từ server. Phản hồi sẽ được xử lý thông qua callback hoặc promises sau khi server trả về dữ liệu.
  • Ưu điểm:
    • Cải thiện trải nghiệm người dùng, vì client không bị chặn và có thể thực hiện nhiều thao tác khác trong khi chờ phản hồi.
    • Hiệu suất tốt hơn trong trường hợp có nhiều yêu cầu, vì client có thể gửi nhiều yêu cầu mà không cần chờ phản hồi từ từng yêu cầu.
  • Nhược điểm:
    • Phức tạp hơn trong việc quản lý luồng thực thi và xử lý callback.
    • Có thể dẫn đến “callback hell” nếu không được tổ chức tốt, làm cho mã khó đọc và bảo trì.

Làm thế nào để đảm bảo an toàn dữ liệu người dùng trong ứng dụng iOS?

Để đảm bảo an toàn dữ liệu người dùng trong ứng dụng iOS, tôi thường áp dụng các phương pháp sau:

  1. Mã hóa dữ liệu:
    • Sử dụng mã hóa mạnh mẽ (như AES-256) để bảo vệ dữ liệu nhạy cảm trước khi lưu trữ hoặc truyền tải. Điều này giúp đảm bảo rằng ngay cả khi dữ liệu bị rò rỉ, nó cũng không thể được truy cập dễ dàng.
  2. Sử dụng Keychain:
    • Lưu trữ thông tin nhạy cảm như mật khẩu và mã xác thực trong Keychain của iOS. Keychain cung cấp một cách an toàn để lưu trữ thông tin nhạy cảm và tự động mã hóa dữ liệu.
  3. Xác thực người dùng:
    • Sử dụng phương thức xác thực mạnh mẽ, chẳng hạn như xác thực hai yếu tố (2FA), để tăng cường bảo mật cho tài khoản người dùng.
    • Tích hợp với các dịch vụ như Firebase Authentication hoặc OAuth để quản lý và bảo vệ thông tin xác thực.
  4. Kiểm tra quyền truy cập:
    • Thực hiện kiểm tra quyền truy cập nghiêm ngặt để đảm bảo rằng người dùng chỉ có thể truy cập dữ liệu mà họ có quyền. Sử dụng các phương pháp như phân quyền và kiểm tra trạng thái phiên làm việc.
  5. Bảo mật mạng:
    • Sử dụng HTTPS để mã hóa dữ liệu khi truyền tải qua mạng. Điều này giúp bảo vệ dữ liệu khỏi việc bị chặn và nghe lén trong quá trình truyền tải.
    • Kiểm tra chứng chỉ SSL để đảm bảo rằng server mà ứng dụng kết nối là hợp lệ và an toàn.
  6. Chống tấn công XSS và SQL Injection:
    • Kiểm soát và xác thực dữ liệu đầu vào từ người dùng để ngăn chặn các tấn công như XSS (Cross-Site Scripting) và SQL Injection. Sử dụng các biện pháp như escaping và prepared statements để bảo vệ ứng dụng.
  7. Cập nhật và vá lỗi thường xuyên:
    • Theo dõi và cập nhật ứng dụng thường xuyên để khắc phục các lỗ hổng bảo mật. Đảm bảo sử dụng các thư viện và SDK mới nhất với các bản vá bảo mật đã được áp dụng.
  8. Quản lý thời gian phiên:
    • Thiết lập thời gian hết hạn cho phiên làm việc của người dùng, tự động đăng xuất nếu không hoạt động trong một khoảng thời gian nhất định.

NSUserDefaults là gì? Khi nào nên sử dụng nó và khi nào không?

NSUserDefaults là một lớp trong iOS cho phép lưu trữ và quản lý các thông tin cấu hình hoặc dữ liệu nhỏ mà ứng dụng cần. Nó cung cấp một cách đơn giản để lưu trữ các giá trị dạng key-value, bao gồm các kiểu dữ liệu như String, Int, Bool, Array, và Dictionary. Dữ liệu lưu trữ trong NSUserDefaults sẽ được duy trì giữa các lần khởi động ứng dụng.

Khi nào nên sử dụng NSUserDefaults?

  1. Lưu trữ cấu hình người dùng:
    • Khi bạn cần lưu trữ các tùy chọn hoặc thiết lập của người dùng như chế độ sáng/tối, ngôn ngữ, hoặc các thông số cấu hình khác mà không cần lưu trữ chúng trong cơ sở dữ liệu.
  2. Dữ liệu nhỏ và không nhạy cảm:
    • Khi bạn cần lưu trữ một lượng dữ liệu nhỏ (ví dụ như trạng thái UI, lần cuối cùng người dùng truy cập một màn hình cụ thể) mà không cần đến hiệu suất cao.
  3. Thông tin cần truy cập nhanh:
    • Khi bạn cần truy cập dữ liệu một cách nhanh chóng mà không cần phải khởi tạo một cơ sở dữ liệu hoặc một file lớn.

Khi nào không nên sử dụng NSUserDefaults?

  1. Dữ liệu lớn hoặc phức tạp:
    • Không nên sử dụng NSUserDefaults để lưu trữ các đối tượng lớn hoặc phức tạp, như hình ảnh hoặc video, vì điều này có thể làm chậm ứng dụng và gây ra các vấn đề về hiệu suất.
  2. Dữ liệu nhạy cảm:
    • Không lưu trữ thông tin nhạy cảm như mật khẩu, thông tin thẻ tín dụng hoặc bất kỳ dữ liệu cá nhân nào trong NSUserDefaults. Thay vào đó, nên sử dụng Keychain để bảo mật thông tin nhạy cảm.
  3. Thông tin cần được tổ chức tốt:
    • Nếu bạn cần lưu trữ một cấu trúc dữ liệu phức tạp hoặc có mối quan hệ giữa các đối tượng, thì sử dụng Core Data hoặc một cơ sở dữ liệu khác sẽ hợp lý hơn.

Sự khác nhau giữa KVO (Key-Value Observing) và NotificationCenter là gì?

Tiêu chí  KVO (Key-Value Observing) NotificationCenter
Định nghĩa  Cơ chế cho phép theo dõi sự thay đổi của thuộc tính Cơ chế cho phép gửi và nhận thông báo giữa các đối tượng
Cách hoạt động  Đăng ký để theo dõi thuộc tính cụ thể Đăng ký để nhận thông báo từ NotificationCenter
Sử dụng  Theo dõi sự thay đổi của thuộc tính Thông báo về sự kiện mà nhiều đối tượng có thể quan tâm
Ưu điểm – Thông báo tức thời khi có sự thay đổi

– Giúp đồng bộ hóa dữ liệu

– Linh hoạt hơn trong việc gửi và nhận thông báo

– Có thể gửi thông báo đến nhiều đối tượng

Nhược điểm  – Cần quản lý đăng ký và hủy đăng ký chính xác

– Chỉ theo dõi thuộc tính cụ thể

– Có thể khó quản lý nhiều observer và thông báo

– Không tự động dọn dẹp observer

Tình huống sử dụng Khi cần đồng bộ hóa giao diện người dùng với dữ liệu Khi cần thông báo về sự kiện chung mà không cần biết đối tượng nhận

Làm thế nào để triển khai các yêu cầu mạng có thời gian dài với URLSession và tác vụ nền (background task)?

Tạo một URLSessionConfiguration với Background Configuration

Trước tiên, bạn cần tạo một phiên URLSession với cấu hình background. Điều này cho phép yêu cầu mạng tiếp tục thực hiện ngay cả khi ứng dụng không hoạt động.

let config = URLSessionConfiguration.background(withIdentifier: "com.example.app.background")

let session = URLSession(configuration: config, delegate: self, delegateQueue: nil)

Thực hiện yêu cầu mạng

Sử dụng URLSession để thực hiện yêu cầu mạng. Bạn có thể sử dụng các phương thức như dataTask(with:completionHandler:) hoặc uploadTask(with:from:) tùy thuộc vào yêu cầu của bạn.

let url = URL(string: "https://example.com/large-file")!

let task = session.downloadTask(with: url)

task.resume()

Xử lý kết quả trong Delegate

Cần đảm bảo rằng bạn tuân theo giao thức URLSessionDelegate, URLSessionTaskDelegate, hoặc URLSessionDownloadDelegate để nhận thông báo về tiến trình tải xuống hoặc hoàn tất. Dưới đây là một ví dụ về cách xử lý hoàn tất tải xuống.

extension YourClass: URLSessionDownloadDelegate {
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        // Xử lý file tải về
    }

    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
        print("Tiến trình tải xuống: \(progress * 100)%")
    }
}

Xử lý Background Task

Khi ứng dụng chuyển sang nền, bạn có thể cần yêu cầu thêm thời gian để hoàn thành yêu cầu mạng. Để làm điều này, bạn có thể sử dụng beginBackgroundTask(withName:expirationHandler:).

var backgroundTask: UIBackgroundTaskIdentifier = .invalid

backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "NetworkRequest") {
    // Xử lý khi background task hết thời gian
    UIApplication.shared.endBackgroundTask(backgroundTask)
}

let task = session.dataTask(with: url) { (data, response, error) in
    // Xử lý yêu cầu mạng
    UIApplication.shared.endBackgroundTask(backgroundTask)
}
task.resume()

Xử lý Kết thúc Background Task

Khi yêu cầu mạng hoàn tất, hãy đảm bảo gọi endBackgroundTask(_:) để kết thúc background task và giải phóng tài nguyên.

Kiểm tra trạng thái tải xuống

Bạn có thể theo dõi trạng thái tải xuống bằng cách kiểm tra thông báo từ delegate hoặc lưu trữ trạng thái trong UserDefaults hoặc một hệ thống lưu trữ khác để cập nhật cho người dùng.

Làm thế nào để hạn chế đến mức thấp nhất hiện tượng bị Crash (hay tắt đột ngột) của ứng dụng iOS?

Để hạn chế hiện tượng crash (tắt đột ngột) của ứng dụng iOS, bạn có thể áp dụng các phương pháp sau:

Xử lý lỗi tốt

  • Sử dụng do-catch: Đảm bảo rằng bạn sử dụng do-catch để xử lý các lỗi khi thực hiện các tác vụ có thể gây ra lỗi, chẳng hạn như đọc file hoặc thực hiện yêu cầu mạng.
  • Kiểm tra optional: Sử dụng if let hoặc guard let để kiểm tra các giá trị optional trước khi sử dụng chúng, giúp tránh lỗi khi truy cập vào giá trị nil.

Giám sát và ghi log

  • Sử dụng công cụ giám sát: Sử dụng các công cụ như Firebase Crashlytics hoặc Sentry để theo dõi và ghi lại thông tin về crash. Điều này giúp bạn nắm bắt được các nguyên nhân gây ra lỗi và khắc phục chúng kịp thời.
  • Ghi log: Ghi lại các sự kiện quan trọng và lỗi trong ứng dụng để giúp bạn phân tích nguyên nhân crash khi có vấn đề xảy ra.

Tối ưu hóa hiệu suất

  • Quản lý bộ nhớ: Tránh rò rỉ bộ nhớ bằng cách sử dụng weakunowned cho các tham chiếu vòng lặp trong các closure và delegate.
  • Kiểm tra hiệu suất: Sử dụng Instruments để phân tích hiệu suất của ứng dụng và phát hiện các vấn đề có thể dẫn đến crash, chẳng hạn như việc sử dụng quá nhiều bộ nhớ.

Kiểm tra đầu vào

  • Kiểm tra dữ liệu đầu vào: Đảm bảo rằng dữ liệu đầu vào (ví dụ: từ người dùng hoặc từ API) là hợp lệ trước khi sử dụng chúng trong ứng dụng.
  • Xử lý dữ liệu bất hợp lệ: Cung cấp các thông báo lỗi rõ ràng và hợp lý cho người dùng khi có dữ liệu không hợp lệ.

Thực hiện test

  • Kiểm tra tự động: Viết các bài kiểm tra tự động (unit tests và UI tests) để kiểm tra các chức năng của ứng dụng. Điều này giúp phát hiện lỗi trong giai đoạn phát triển.
  • Kiểm tra trên các thiết bị khác nhau: Thực hiện kiểm tra trên nhiều loại thiết bị và phiên bản iOS khác nhau để đảm bảo rằng ứng dụng hoạt động ổn định trên tất cả các môi trường.

Quản lý background tasks

  • Kết thúc các task không cần thiết: Đảm bảo rằng bạn quản lý các background tasks một cách hiệu quả và kết thúc chúng khi không còn cần thiết.
  • Sử dụng đúng APIs: Đối với các tác vụ mạng dài, hãy sử dụng URLSession với background configuration để đảm bảo rằng chúng có thể tiếp tục chạy ngay cả khi ứng dụng không hoạt động.

Cập nhật thư viện và công nghệ

  • Cập nhật thường xuyên: Đảm bảo rằng bạn sử dụng các phiên bản mới nhất của các thư viện và framework để tận dụng các bản sửa lỗi và cải tiến hiệu suất.
  • Theo dõi các thay đổi trong iOS: Luôn cập nhật kiến thức về các thay đổi trong API và công nghệ mới để đảm bảo ứng dụng của bạn tương thích và hoạt động ổn định.

Tổng kết câu hỏi phỏng vấn iOS Developer

Việc chuẩn bị cho các cuộc phỏng vấn là điều cần thiết để đạt được thành công. Bài viết này đã trình bày một loạt câu hỏi phỏng vấn iOS Developer phổ biến liên quan đến Swift, UIKit, SwiftUI và các công nghệ khác, cùng với những câu trả lời ngắn gọn nhưng đầy đủ ý. Qua đó, bạn không chỉ nắm vững các khái niệm và kỹ thuật quan trọng mà còn có cơ hội rèn luyện khả năng giải thích và trình bày kiến thức của mình.