modal View controller - cách hiển thị và loại bỏ


82

Một tuần qua, tôi đang suy nghĩ nhiều về cách giải quyết vấn đề khi hiển thị và loại bỏ nhiều bộ điều khiển chế độ xem. Tôi đã tạo một dự án mẫu và dán mã trực tiếp từ dự án. Tôi có 3 bộ điều khiển chế độ xem với các tệp .xib tương ứng của chúng. MainViewController, VC1 và VC2. Tôi có hai nút trên bộ điều khiển chế độ xem chính.

- (IBAction)VC1Pressed:(UIButton *)sender
{
    VC1 *vc1 = [[VC1 alloc] initWithNibName:@"VC1" bundle:nil];
    [vc1 setModalTransitionStyle:UIModalTransitionStyleFlipHorizontal];
    [self presentViewController:vc1 animated:YES completion:nil];
}

Điều này sẽ mở VC1 mà không có vấn đề gì. Trong VC1, tôi có một nút khác sẽ mở VC2 trong khi đồng thời loại bỏ VC1.

- (IBAction)buttonPressedFromVC1:(UIButton *)sender
{
    VC2 *vc2 = [[VC2 alloc] initWithNibName:@"VC2" bundle:nil];
    [vc2 setModalTransitionStyle:UIModalTransitionStyleFlipHorizontal];
    [self presentViewController:vc2 animated:YES completion:nil];
    [self dismissViewControllerAnimated:YES completion:nil];
} // This shows a warning: Attempt to dismiss from view controller <VC1: 0x715e460> while a presentation or dismiss is in progress!


- (IBAction)buttonPressedFromVC2:(UIButton *)sender
{
    [self dismissViewControllerAnimated:YES completion:nil];
} // This is going back to VC1. 

Tôi muốn nó quay trở lại bộ điều khiển chế độ xem chính đồng thời VC1 lẽ ra phải được xóa khỏi bộ nhớ. VC1 chỉ hiển thị khi tôi nhấp vào nút VC1 trên bộ điều khiển chính.

Nút khác trên Bộ điều khiển chế độ xem chính cũng có thể hiển thị trực tiếp VC2 bỏ qua VC1 và sẽ quay lại bộ điều khiển chính khi một nút được nhấp trên VC2. Không có mã chạy dài, vòng lặp hoặc bất kỳ bộ hẹn giờ nào. Chỉ cần gọi xương trần để xem bộ điều khiển.

Câu trả lời:


189

Đường thẳng này:

[self dismissViewControllerAnimated:YES completion:nil];

không phải gửi một thông điệp cho chính nó, nó thực sự đang gửi một thông điệp đến VC đang trình bày của nó, yêu cầu nó thực hiện việc loại bỏ. Khi bạn trình bày một VC, bạn tạo ra mối quan hệ giữa VC trình bày và VC được trình bày. Vì vậy, bạn không nên hủy VC đang trình bày khi nó đang trình bày (VC đã trình bày không thể gửi lại thông báo loại bỏ đó…). Vì bạn không thực sự tính đến nó, bạn đang để ứng dụng ở trạng thái bối rối. Xem câu trả lời của tôi Loại bỏ Bộ điều khiển dạng xem được trình bày trong đó tôi đề xuất phương pháp này được viết rõ ràng hơn:

[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];

Trong trường hợp của bạn, bạn cần đảm bảo rằng tất cả các kiểm soát được thực hiện trong mainVC . Bạn nên sử dụng một đại biểu để gửi thông báo chính xác trở lại MainViewController từ ViewController1, để mainVC có thể loại bỏ VC1 và sau đó trình bày VC2.

Trong VC2 VC1, hãy thêm một giao thức trong tệp .h của bạn phía trên @interface:

@protocol ViewController1Protocol <NSObject>

    - (void)dismissAndPresentVC2;

@end

và hạ xuống trong cùng một tệp trong phần @interface, khai báo một thuộc tính để giữ con trỏ đại biểu:

@property (nonatomic,weak) id <ViewController1Protocol> delegate;

Trong tệp VC1 .m, phương thức nút loại bỏ sẽ gọi phương thức ủy quyền

- (IBAction)buttonPressedFromVC1:(UIButton *)sender {
    [self.delegate dissmissAndPresentVC2]
}

Bây giờ trong mainVC, hãy đặt nó làm đại biểu của VC1 khi tạo VC1:

- (IBAction)present1:(id)sender {
    ViewController1* vc = [[ViewController1 alloc] initWithNibName:@"ViewController1" bundle:nil];
    vc.delegate = self;
    [self present:vc];
}

và thực hiện phương pháp ủy quyền:

- (void)dismissAndPresent2 {
    [self dismissViewControllerAnimated:NO completion:^{
        [self present2:nil];
    }];
}

present2:có thể là phương pháp giống như phương pháp VC2Pressed:IBAction nút của bạn . Lưu ý rằng nó được gọi từ khối hoàn thành để đảm bảo rằng VC2 không được hiển thị cho đến khi VC1 bị loại bỏ hoàn toàn.

Bây giờ bạn đang di chuyển từ VC1-> VCMain-> VC2, vì vậy bạn có thể chỉ muốn một trong các chuyển đổi được làm động.

cập nhật

Trong nhận xét của mình, bạn bày tỏ sự ngạc nhiên về sự phức tạp cần thiết để đạt được một điều tưởng chừng như đơn giản. Tôi đảm bảo với bạn, mô hình ủy quyền này rất trung tâm đối với phần lớn Objective-C và Cocoa, và ví dụ này là về điều đơn giản nhất bạn có thể nhận được, mà bạn thực sự nên cố gắng để làm cho thoải mái với nó.

Trong Hướng dẫn lập trình bộ điều khiển chế độ xem của Apple, họ có điều này để nói :

Loại bỏ bộ điều khiển dạng xem được trình bày

Khi đến lúc loại bỏ bộ điều khiển dạng xem đã trình bày, phương pháp ưu tiên là để bộ điều khiển dạng xem trình bày loại bỏ nó. Nói cách khác, bất cứ khi nào có thể, bộ điều khiển chế độ xem tương tự đã trình bày bộ điều khiển chế độ xem cũng phải chịu trách nhiệm loại bỏ nó. Mặc dù có một số kỹ thuật để thông báo cho bộ điều khiển chế độ xem trình bày rằng bộ điều khiển chế độ xem đã trình bày của nó nên bị loại bỏ, kỹ thuật được ưu tiên là ủy quyền. Để biết thêm thông tin, hãy xem “Sử dụng ủy quyền để giao tiếp với những người kiểm soát khác”.

Nếu bạn thực sự suy nghĩ kỹ về những gì bạn muốn đạt được và cách bạn thực hiện nó, bạn sẽ nhận ra rằng việc nhắn tin cho MainViewController của bạn để thực hiện tất cả công việc là cách giải quyết hợp lý duy nhất khiến bạn không muốn sử dụng NavigationController. Nếu bạn làm sử dụng một NavController, có hiệu lực bạn là 'ủy thác', thậm chí nếu không muốn nói một cách rõ ràng, để các navController để làm tất cả công việc. Cần có một số đối tượng theo dõi trung tâm những gì đang diễn ra với điều hướng VC của bạn và bạn cần một số phương pháp giao tiếp với nó, bất cứ điều gì bạn làm.

Trên thực tế, lời khuyên của Apple hơi cực đoan ... trong những trường hợp bình thường, bạn không cần phải đưa ra một phương pháp và người ủy quyền chuyên dụng, bạn có thể dựa vào [self presentingViewController] dismissViewControllerAnimated:- đó là khi trong những trường hợp như của bạn, bạn muốn việc sa thải của mình có những tác động khác trên điều khiển từ xa đối tượng mà bạn cần phải chăm sóc.

Đây là một cái gì đó bạn có thể tưởng tượng để làm việc mà không có tất cả các rắc rối của đại biểu ...

- (IBAction)dismiss:(id)sender {
    [[self presentingViewController] dismissViewControllerAnimated:YES 
                                                        completion:^{
        [self.presentingViewController performSelector:@selector(presentVC2:) 
                                            withObject:nil];
    }];

}

Sau khi yêu cầu trình điều khiển trình bày loại bỏ chúng tôi, chúng tôi có một khối hoàn thành gọi một phương thức trong PresentationViewController để gọi VC2. Không cần đại biểu. (Một điểm hấp dẫn lớn của các khối là họ giảm nhu cầu về các đại biểu trong những trường hợp này). Tuy nhiên, trong trường hợp này, có một số điều cản trở ...

  • trong VC1, bạn không biết rằng mainVC thực hiện phương thức present2- bạn có thể gặp phải các lỗi hoặc sự cố khó gỡ lỗi. Đại biểu giúp bạn để tránh điều này.
  • một khi VC1 bị loại bỏ, nó không thực sự xung quanh để thực thi khối hoàn thành ... hay là? Self.presentingViewController có nghĩa là gì nữa không? Bạn không biết (tôi cũng vậy) ... với một đại biểu, bạn không có sự không chắc chắn này.
  • Khi tôi cố gắng chạy phương pháp này, nó chỉ bị treo mà không có cảnh báo hoặc lỗi.

Vì vậy, xin vui lòng ... dành thời gian để tìm hiểu đoàn!

update2

Trong nhận xét của bạn, bạn đã quản lý để làm cho nó hoạt động bằng cách sử dụng điều này trong trình xử lý nút loại bỏ của VC2:

 [self.view.window.rootViewController dismissViewControllerAnimated:YES completion:nil]; 

Điều này chắc chắn đơn giản hơn nhiều, nhưng nó để lại cho bạn một số vấn đề.

Khớp nối chặt chẽ
Bạn đang kết nối cấu trúc viewController với nhau. Ví dụ: nếu bạn chèn một viewController mới trước mainVC, hành vi bắt buộc của bạn sẽ bị hỏng (bạn sẽ điều hướng đến hành vi trước đó). Trong VC1, bạn cũng phải # nhập VC2. Do đó, bạn có khá nhiều phụ thuộc lẫn nhau, điều này phá vỡ các mục tiêu OOP / MVC.

Khi sử dụng các đại biểu, cả VC1 và VC2 đều không cần biết bất kỳ điều gì về mainVC hoặc các tiền thân của nó, vì vậy chúng tôi giữ mọi thứ được kết hợp lỏng lẻo và mô-đun.

Bộ nhớ
VC1 vẫn chưa biến mất, bạn vẫn giữ hai con trỏ tới nó:

  • presentedViewControllertài sản của mainVC
  • presentingViewControllerTài sản của VC2

Bạn có thể kiểm tra điều này bằng cách ghi nhật ký và cũng chỉ bằng cách thực hiện điều này từ VC2

[self dismissViewControllerAnimated:YES completion:nil]; 

Nó vẫn hoạt động, vẫn đưa bạn trở lại VC1.

Điều đó đối với tôi dường như là một sự rò rỉ ký ức.

Manh mối cho điều này nằm trong cảnh báo bạn đang nhận được ở đây:

[self presentViewController:vc2 animated:YES completion:nil];
[self dismissViewControllerAnimated:YES completion:nil];
 // Attempt to dismiss from view controller <VC1: 0x715e460>
 // while a presentation or dismiss is in progress!

Logic bị phá vỡ, vì bạn đang cố gắng loại bỏ VC trình bày trong đó VC2 là VC trình bày. Thông báo thứ hai không thực sự được thực thi - có lẽ một số điều xảy ra, nhưng bạn vẫn còn lại hai con trỏ đến một đối tượng mà bạn nghĩ rằng bạn đã loại bỏ. ( chỉnh sửa - Tôi đã kiểm tra điều này và nó không quá tệ, cả hai đối tượng đều biến mất khi bạn quay lại mainVC )

Đó là một cách nói khá dài dòng - làm ơn, hãy sử dụng các đại biểu. Nếu nó hữu ích, tôi đã thực hiện một mô tả ngắn gọn khác về mẫu ở đây: Việc
chuyển một bộ điều khiển trong một trình tạo có luôn là một phương pháp không tốt không?

cập nhật 3
Nếu bạn thực sự muốn tránh người được ủy quyền, đây có thể là cách tốt nhất:

Trong VC1:

[self presentViewController:VC2
                   animated:YES
                 completion:nil];

Nhưng đừng bác bỏ bất cứ điều gì ... như chúng tôi đã xác định, nó không thực sự xảy ra.

Trong VC2:

[self.presentingViewController.presentingViewController 
    dismissViewControllerAnimated:YES
                       completion:nil];

Như chúng tôi (biết) chúng tôi chưa loại bỏ VC1, chúng tôi có thể liên hệ lại thông qua VC1 đến MainVC. MainVC loại bỏ VC1. Bởi vì VC1 đã biến mất, VC2 được trình bày đi cùng với nó, vì vậy bạn quay lại MainVC trong trạng thái sạch sẽ.

Nó vẫn được kết hợp chặt chẽ, vì VC1 cần biết về VC2 và VC2 cần biết rằng nó đã được đến qua MainVC-> VC1, nhưng đó là điều tốt nhất bạn sẽ nhận được mà không cần một chút ủy quyền rõ ràng.


1
có vẻ là phức tạp. Tôi đã cố gắng làm theo và chép vào dấu chấm nhưng bị lạc giữa chừng. Có cách nào khác để đạt được điều này không ?. Tôi cũng muốn thêm rằng trong đại biểu ứng dụng, bộ điều khiển chính được đặt làm bộ điều khiển chế độ xem gốc. Tôi không muốn sử dụng bộ điều khiển Điều hướng nhưng tự hỏi tại sao điều này lại phức tạp như vậy để đạt được. Tóm lại, khi ứng dụng khởi động, tôi hiển thị một bộ điều khiển chế độ xem chính có 2 nút. Nhấp vào nút đầu tiên tải VC1. Có một nút trên VC1 và khi nhấp vào nó sẽ tải VC2 mà không có lỗi hoặc cảnh báo nào đồng thời loại bỏ VC1 khỏi bộ nhớ.
Hema

Trên VC2, tôi có một nút và nhấp vào nó sẽ loại bỏ VC2 khỏi bộ nhớ và điều khiển sẽ quay trở lại bộ điều khiển chính chứ không phải đến VC1.
Hema

@Hema, tôi đã hoàn toàn hiểu yêu cầu của bạn và đảm bảo với bạn rằng đây cách chính xác để thực hiện. Tôi đã cập nhật câu trả lời của mình với một ít thông tin hơn, hy vọng điều đó sẽ hữu ích. Nếu bạn đã thử cách tiếp cận của tôi và gặp khó khăn, vui lòng đưa ra một câu hỏi mới hiển thị chính xác những gì không hoạt động để chúng tôi có thể trợ giúp. Bạn cũng có thể liên kết lại câu hỏi này cho rõ ràng.
đúc

Hi He Was: Cảm ơn vì cái nhìn sâu sắc của bạn. Tôi cũng đang nói về một chủ đề khác (chủ đề gốc) và chỉ đăng một đoạn trích từ các đề xuất được đề cập ở đó. Tôi đang thử tất cả các câu trả lời của chuyên gia để giải quyết vấn đề này. URL ở đây: stackoverflow.com/questions/14840318/…
Hema

1
@Honey - Có lẽ vậy, nhưng tuyên bố là một câu trả lời tu từ cho một đoạn mã giả 'tưởng tượng'. Điểm tôi muốn làm không phải là về bẫy chu kỳ giữ lại, mà là để hướng dẫn người hỏi về lý do tại sao ủy quyền lại là một mẫu thiết kế có giá trị (điều này tình cờ tránh được vấn đề đó). Tôi nghĩ rằng đó là sự tranh cãi gây hiểu lầm ở đây - câu hỏi là về các VC phương thức, nhưng giá trị của câu trả lời chủ yếu nằm ở việc giải thích mẫu đại biểu, sử dụng câu hỏi và sự thất vọng rõ ràng của OP làm chất xúc tác. Cảm ơn sự quan tâm của bạn (và các chỉnh sửa của bạn) !!
đúc

12

Ví dụ trong Swift , hình dung lời giải thích của xưởng đúc ở trên và tài liệu của Apple:

  1. Dựa trên tài liệu của Apple và giải thích của xưởng đúc ở trên (sửa một số lỗi), phiên bản presentViewController sử dụng mẫu thiết kế ủy quyền:

ViewController.swift

import UIKit

protocol ViewControllerProtocol {
    func dismissViewController1AndPresentViewController2()
}

class ViewController: UIViewController, ViewControllerProtocol {

    @IBAction func goToViewController1BtnPressed(sender: UIButton) {
        let vc1: ViewController1 = self.storyboard?.instantiateViewControllerWithIdentifier("VC1") as ViewController1
        vc1.delegate = self
        vc1.modalTransitionStyle = UIModalTransitionStyle.FlipHorizontal
        self.presentViewController(vc1, animated: true, completion: nil)
    }

    func dismissViewController1AndPresentViewController2() {
        self.dismissViewControllerAnimated(false, completion: { () -> Void in
            let vc2: ViewController2 = self.storyboard?.instantiateViewControllerWithIdentifier("VC2") as ViewController2
            self.presentViewController(vc2, animated: true, completion: nil)
        })
    }

}

ViewController1.swift

import UIKit

class ViewController1: UIViewController {

    var delegate: protocol<ViewControllerProtocol>!

    @IBAction func goToViewController2(sender: UIButton) {
        self.delegate.dismissViewController1AndPresentViewController2()
    }

}

ViewController2.swift

import UIKit

class ViewController2: UIViewController {

}
  1. Dựa trên giải thích của xưởng đúc ở trên (sửa một số lỗi), phiên bản pushViewController sử dụng mẫu thiết kế đại biểu:

ViewController.swift

import UIKit

protocol ViewControllerProtocol {
    func popViewController1AndPushViewController2()
}

class ViewController: UIViewController, ViewControllerProtocol {

    @IBAction func goToViewController1BtnPressed(sender: UIButton) {
        let vc1: ViewController1 = self.storyboard?.instantiateViewControllerWithIdentifier("VC1") as ViewController1
        vc1.delegate = self
        self.navigationController?.pushViewController(vc1, animated: true)
    }

    func popViewController1AndPushViewController2() {
        self.navigationController?.popViewControllerAnimated(false)
        let vc2: ViewController2 = self.storyboard?.instantiateViewControllerWithIdentifier("VC2") as ViewController2
        self.navigationController?.pushViewController(vc2, animated: true)
    }

}

ViewController1.swift

import UIKit

class ViewController1: UIViewController {

    var delegate: protocol<ViewControllerProtocol>!

    @IBAction func goToViewController2(sender: UIButton) {
        self.delegate.popViewController1AndPushViewController2()
    }

}

ViewController2.swift

import UIKit

class ViewController2: UIViewController {

}

trong ViewControllerlớp ví dụ của bạn là mainVC phải không?
Honey

10

Tôi nghĩ rằng bạn đã hiểu sai một số khái niệm cốt lõi về bộ điều khiển chế độ xem phương thức iOS. Khi bạn loại bỏ VC1, mọi bộ điều khiển chế độ xem được trình bày bởi VC1 cũng bị loại bỏ. Apple dành cho các bộ điều khiển chế độ xem phương thức chạy theo cách xếp chồng lên nhau - trong trường hợp của bạn, VC2 được trình bày bởi VC1. Bạn đang loại bỏ VC1 ngay sau khi bạn trình bày VC2 từ VC1, vì vậy đó là một mớ hỗn độn. Để đạt được những gì bạn muốn, buttonPressedFromVC1 phải có VC2 chính hiện diện ngay sau khi VC1 tự loại bỏ. Và tôi nghĩ rằng điều này có thể đạt được mà không cần đại biểu. Một cái gì đó dọc theo dòng:

UIViewController presentingVC = [self presentingViewController];
[self dismissViewControllerAnimated:YES completion:
 ^{
    [presentingVC presentViewController:vc2 animated:YES completion:nil];
 }];

Lưu ý rằng self.presentingViewController được lưu trữ trong một số biến khác, vì sau khi vc1 tự loại bỏ, bạn không nên tạo bất kỳ tham chiếu nào đến nó.


1
Quá dễ! Tôi ước những người khác sẽ cuộn xuống câu trả lời của bạn thay vì dừng lại ở bài đăng trên cùng.
Ryan Loggerythm

trong mã OP, tại sao không [self dismiss...]xảy ra sau khi [self present...] kết thúc? Nó không có cái gì đó không đồng bộ xảy ra
Mật ong

1
@Honey thực sự, có một cái gì đó không đồng bộ xảy ra khi gọi presentViewController - đó là lý do tại sao nó có một trình xử lý hoàn thành. Nhưng ngay cả khi sử dụng điều đó, nếu bạn loại bỏ trình điều khiển chế độ xem trình bày sau khi nó trình bày nội dung nào đó, mọi thứ mà nó trình bày cũng bị loại bỏ. Vì vậy, OP thực sự muốn trình bày các viewController từ người dẫn chương trình khác trên thực tế, để nó có thể bỏ qua một trong hiện tại
Radu Simionescu

Nhưng ngay cả khi sử dụng điều đó, nếu bạn loại bỏ trình điều khiển chế độ xem trình bày sau khi nó trình bày một thứ gì đó, mọi thứ mà nó trình bày cũng bị loại bỏ ... Aha, vì vậy trình biên dịch về cơ bản đang nói "những gì bạn làm là ngu ngốc. Bạn chỉ cần hoàn tác chỉnh sửa trước đó của mình dòng mã (như VC1 tôi sẽ tự loại bỏ và bất cứ điều gì tôi đang trình bày). Đừng làm điều đó "phải không?
Honey,

Trình biên dịch sẽ không "nói" bất cứ điều gì về nó, và nó cũng có thể là trường hợp để không sụp đổ khi thực hiện điều này, chỉ rằng nó sẽ cư xử theo một cách mà các lập trình viên không mong đợi
Radu Simionescu

5

Radu Simionescu - tác phẩm tuyệt vời! và bên dưới Giải pháp của bạn cho những người yêu thích Swift:

@IBAction func showSecondControlerAndCloseCurrentOne(sender: UIButton) {
    let secondViewController = storyboard?.instantiateViewControllerWithIdentifier("ConrollerStoryboardID") as UIViewControllerClass // change it as You need it
    var presentingVC = self.presentingViewController
    self.dismissViewControllerAnimated(false, completion: { () -> Void   in
        presentingVC!.presentViewController(secondViewController, animated: true, completion: nil)
    })
}

theo một cách nào đó khiến tôi bực bội vì nó thực sự hoạt động .. Tôi không hiểu tại sao khối không nắm bắt được "self.presentingViewController" và cần có một tham chiếu mạnh, tức là "var presentVC" .. dù sao thì điều này cũng hoạt động. thx
emdog 4

1

Tôi muốn cái này:

MapVC là Bản đồ toàn màn hình.

Khi tôi nhấn một nút, nó sẽ mở PopupVC (không ở chế độ toàn màn hình) phía trên bản đồ.

Khi tôi nhấn một nút trong PopupVC, nó sẽ quay trở lại MapVC, sau đó tôi muốn thực thi viewDidAppear.

Tôi đã làm điều này:

MapVC.m: trong hành động nút, xác định theo lập trình và đặt đại biểu

- (void) buttonMapAction{
   PopupVC *popvc = [self.storyboard instantiateViewControllerWithIdentifier:@"popup"];
   popvc.delegate = self;
   [self presentViewController:popvc animated:YES completion:nil];
}

- (void)dismissAndPresentMap {
  [self dismissViewControllerAnimated:NO completion:^{
    NSLog(@"dismissAndPresentMap");
    //When returns of the other view I call viewDidAppear but you can call to other functions
    [self viewDidAppear:YES];
  }];
}

PopupVC.h: trước @interface, hãy thêm giao thức

@protocol PopupVCProtocol <NSObject>
- (void)dismissAndPresentMap;
@end

sau @interface, một thuộc tính mới

@property (nonatomic,weak) id <PopupVCProtocol> delegate;

PopupVC.m:

- (void) buttonPopupAction{
  //jump to dismissAndPresentMap on Map view
  [self.delegate dismissAndPresentMap];
}

1

Tôi đã giải quyết vấn đề bằng cách sử dụng UINavigationController khi trình bày. Trong MainVC, khi trình bày VC1

let vc1 = VC1()
let navigationVC = UINavigationController(rootViewController: vc1)
self.present(navigationVC, animated: true, completion: nil)

Trong VC1, khi tôi muốn hiển thị VC2 và loại bỏ VC1 cùng một lúc (chỉ một hoạt ảnh), tôi có thể có hoạt ảnh đẩy bằng cách

let vc2 = VC2()
self.navigationController?.setViewControllers([vc2], animated: true)

Và trong VC2, khi đóng bộ điều khiển chế độ xem, như thường lệ, chúng ta có thể sử dụng:

self.dismiss(animated: true, completion: nil)
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.