안드로이드를 하면서 제일 고생했던 것은 레이아웃이다.
레이아웃의 개념을 잡는 것도 어렵긴 했는데... ( 디자인 센스가 1도 없어서.. )
서로 다른 디바이스에서 같은 비율로 UI를 보여준다는 것 자체가 이해가 잘 안됬다... 가능한가 ??? 라는 생각이 먼저였다.
안드로이드를 하다보니 weight 개념을 알게되고 어느정도 비율이라는 것을 건드릴 때쯤 iOS에는 AutoLayout이라는 것이 있어서 layout을 자동(?)으로 잡아준다는 이야기를 들었다.
처음 안드로이드 앱을 만들때라서 ( 프로젝트 ) 레이아웃으로 피똥(?)싸고 있는데 ㅠㅠ iOS는 그리 쉬운 방법이 있었다니...
AutoLayout
뷰의 제약 사항을 바탕으로 뷰 체계 내의 모든뷰의 크기와 위치를 동적으로 계산한다. 앱을 사용할 때 발생하는 외부 변경과 내부 변경에 동적으로 반응하는 사용자 인터페이스를 가능하게 한다.
외부 변경 ( External Changes )
- 슈퍼뷰의 크그나 모양이 변경될 때 발생
- 사용자가 아이패드의 분할뷰(Split View)를 사용하거나 사용하지 않는 경우
- 장치를 회전하는 경우
- 활성화콜과 오디오 녹음 바가 보여지거나 사라지는 경우
- 다른 크기의 클래스를 지원할 경우
- 다른 크기의 스크린을 지원할 경우
내부 변경 ( Internal Changes )
- 앱 변경에 의해 콘텐츠가 보여지는 경우
- 국제화를 지원하는 경우
- 동적 타입을 지원하는 경우
오토 레이아웃이 필요한 이유
- 앱이 실행되는 iOS 기기의 스크린 화면의 크기가 다양한 경우
- 스크린이 회전할 수 있는 경우
- 오디오 바가 보여지거나 사라지는 경우
- 콘텐츠가 동적으로 보여지는 경우
- 지역화(Localization)을 지원하는 경우
- 동적 타입을 지원하는 경우
오토레이아웃 속성
Attribute | Description |
Width | 사각형의 너비 |
Height | 사각형의 높이 |
Top | 사각형의 상단 |
Bottom | 사각형의 하단 |
Baseline | 텍스트의 하단 |
Horizontal | 수평 |
Vertical | 수직 |
Leading | 텍스트를 읽을 때 방향 |
Trailing | 텍스트를 읽을 때 끝 방향 |
CenterX | 수평 중심 |
CenterY | 수직 중심 |
iOS에는 고유 콘텐츠 크기(Intrinsic Content Size)가 있는데, 이는 뷰가 원래 갖는 크기라고 한다. 즉 레이블의 고유 컨텐츠 크기는 텍스트의 크기고, 이미지의 고유 콘텐츠 크기는 이미지 자체의 크기다.
이 개념은 제약 우선도(Constraint Priorities)에서 사용되는데, 크기를 줄이거나 늘리는데도 우선순위가 존재한다.
- 콘텐츠 허깅 우선도(Content hugging priority) : 콘텐츠 고유 사이즈보다 뷰가 커지지 않도록 제한. 뷰는 콘텐츠 사이즈 보다 커지지 않는다.
- 콘텐츠 축소 방지 우선도(Content compression resistance priority) : 콘텐츠 고유의 사이즈 보다 작아지지 않도록 제한.
코드로 구현하기
안드로이드와 마찬가지로 UI를 꼭 스토리보드에서 생성하는 것이 아니라 코드로도 생성 할 수 있다.
이 때 우리는 오토레이아웃을 고려해서 작성해야 하는데, 코드로 구현하면 조금은 복잡 할 수 있다.
import UIKit
import AVFoundation
class ViewController: UIViewController, AVAudioPlayerDelegate {
var player : AVAudioPlayer!
var timer : Timer!
@IBOutlet weak var playBtn: UIButton!
@IBOutlet weak var playTimeLabel: UILabel!
@IBOutlet weak var playSilder: UISlider!
// 재생 버튼을 클릭하면
@IBAction func touchUpPlayPauseBtn(_ sender : UIButton){
print("btn change");
sender.isSelected = !sender.isSelected
// sender가 선택되면 플레이어가 재생중인지 확인하고 처리
if sender.isSelected {
self.player?.play()
} else {
self.player?.pause()
}
// sender가 선택되면 타이머 처리를 할 것인지 확인
if sender.isSelected{
self.mamkeAndFireTimer()
} else{
self.invalidateTimer()
}
}
// 플레이어 초기화.
func initializePlayer(){
guard let soundAsset : NSDataAsset = NSDataAsset(name : "sound") else {
print("음원없음");
return;
}
do {
try self.player = AVAudioPlayer(data : soundAsset.data);
self.player.delegate = self;
} catch let error as NSError {
print("초기화 실패");
print("코드 : \(error.code), 메세지 : \(error.localizedDescription) ");
}
self.playSilder.maximumValue = Float(self.player.duration);
self.playSilder.minimumValue = 0;
self.playSilder.value = Float(self.player.currentTime);
}
// 레이블 업데이트.
func updateTimeLabelText(time:TimeInterval){
let minute : Int = Int(time/60);
let second : Int = Int(time.truncatingRemainder(dividingBy: 60));
let milisecond : Int = Int(time.truncatingRemainder(dividingBy: 1)*100);
let timeText : String = String(format : "%02ld:%02ld:%02ld", minute, second, milisecond);
self.playTimeLabel.text = timeText;
}
// 타이머를 만들고 수행해줄 메소드
func mamkeAndFireTimer(){
self.timer = Timer.scheduledTimer(withTimeInterval : 0.01, repeats: true, block : { [unowned self] (timer : Timer) in
if self.playSilder.isTracking { return };
self.updateTimeLabelText(time:self.player.currentTime);
self.playSilder.value = Float(self.player.currentTime);
})
self.timer.fire();
}
// 플레이어 재생 관련 에러처리.
func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) {
guard let error: Error = error else{
print("플레이어 오류 발생");
return
}
let message : String
message = "플레이어 오류 발생 \(error.localizedDescription)";
let alert : UIAlertController = UIAlertController(title:"알림", message : message, preferredStyle :UIAlertController.Style.alert);
let okAction : UIAlertAction = UIAlertAction(title : "확인", style : UIAlertAction.Style.default) { ( action:UIAlertAction) -> Void in
self.dismiss(animated: true, completion:nil)
}
alert.addAction(okAction)
self.present(alert, animated:true, completion:nil)
}
// 음악 재생이 끝나면
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
self.playBtn.isSelected = false;
self.playSilder.value = 0;
self.updateTimeLabelText(time: 0)
self.invalidateTimer()
}
// 타이머 해제
func invalidateTimer(){
self.timer.invalidate();
self.timer = nil;
}
func addViewWithCode(){
self.addPlayPauseButton();
self.addTimeLabel();
self.addProgressSlider();
}
// 버튼 만들기
func addPlayPauseButton(){
let button : UIButton = UIButton(type:UIButton.ButtonType.custom);
button.translatesAutoresizingMaskIntoConstraints = false;
self.view.addSubview(button);
button.setImage(UIImage(named:"button_play"), for: UIControl.State.normal);
button.setImage(UIImage(named:"button_pause"), for: UIControl.State.selected);
button.addTarget(self, action: #selector(self.touchUpPlayPauseBtn(_:)), for: UIControl.Event.touchUpInside);
let centerX : NSLayoutConstraint
centerX = button.centerXAnchor.constraint(equalTo:self.view.centerXAnchor);
let centerY : NSLayoutConstraint
centerY = NSLayoutConstraint(item:button, attribute:NSLayoutConstraint.Attribute.centerY, relatedBy:NSLayoutConstraint.Relation.equal, toItem: self.view, attribute : NSLayoutConstraint.Attribute.centerY, multiplier:0.8, constant:0);
let width:NSLayoutConstraint
width = button.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier : 0.5 );
let ratio : NSLayoutConstraint
ratio = button.heightAnchor.constraint(equalTo: button.widthAnchor, multiplier:1)
centerX.isActive = true
centerY.isActive = true;
width.isActive = true;
ratio.isActive = true;
self.playBtn = button;
}
//타임 라벨 만들기
func addTimeLabel(){
let timeLabel : UILabel = UILabel()
timeLabel.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(timeLabel);
timeLabel.textColor = UIColor.black
timeLabel.textAlignment = NSTextAlignment.center
timeLabel.font = UIFont.preferredFont(forTextStyle : UIFont.TextStyle.headline);
let centerX: NSLayoutConstraint
centerX = timeLabel.centerXAnchor.constraint(equalTo: self.playBtn.centerXAnchor);
let top : NSLayoutConstraint
top = timeLabel.topAnchor.constraint(equalTo : self.playBtn.bottomAnchor, constant:8);
centerX.isActive = true;
top.isActive = true;
self.playTimeLabel = timeLabel;
self.updateTimeLabelText(time: 0);
}
// 슬라이더 만들기
func addProgressSlider(){
let slider : UISlider = UISlider();
slider.translatesAutoresizingMaskIntoConstraints = false;
self.view.addSubview(slider);
slider.addTarget(self, action:#selector(self.sliderValueControl(_:)), for:UIControl.Event.valueChanged);
let safeAreaGuide : UILayoutGuide = self.view.safeAreaLayoutGuide
let centerX : NSLayoutConstraint
centerX = slider.centerXAnchor.constraint(equalTo : self.playTimeLabel.centerXAnchor);
let top: NSLayoutConstraint
top = slider.topAnchor.constraint(equalTo: self.playTimeLabel.bottomAnchor, constant: 8)
let leading : NSLayoutConstraint
leading = slider.leadingAnchor.constraint(equalTo: safeAreaGuide.leadingAnchor, constant: 16)
let trailing : NSLayoutConstraint
trailing = slider.trailingAnchor.constraint(equalTo: safeAreaGuide.trailingAnchor, constant: -16);
centerX.isActive = true;
top.isActive = true;
leading.isActive = true;
trailing.isActive = true;
self.playSilder = slider;
}
override func viewDidLoad() {
super.viewDidLoad()
self.addViewWithCode();
self.initializePlayer();
// Do any additional setup after loading the view, typically from a nib.
}
// 슬라이더를 이용한 오디오 제어
@IBAction func sliderValueControl(_ sender: UISlider) {
self.updateTimeLabelText(time: TimeInterval(sender.value))
if sender.isTracking{return}
self.player.currentTime = TimeInterval(sender.value);
}
}
여기서 핵심은 AutoLayout을 이용하려면 아래 코드가 들어가야 충돌없이 가능하다.
translatesAutoresizingMaskIntoConstraints = false
이 코드는 View에 충돌하지 않는 constraint집합을 제공한다고 하는데.. 아 아직은 잘 모르겠다..
참조할 만한 사이트를 링크한다.. 댓글 보니 많은 도움을 받았다고 하는데.. 나는 아직 이해가 잘 안가서 뭔소린지 ...ㅋㅋㅋ;;;
'Develop > iOS' 카테고리의 다른 글
iOS 음악 재생 앱 만들기 Part#2 - Audio Method (0) | 2019.05.14 |
---|---|
iOS 음악 재생 앱 만들기 Part#1 - 앱 개발 (0) | 2019.05.14 |
Swift 오류 처리 ( Error ) (0) | 2019.05.13 |
Swift 익스텐션 (0) | 2019.05.13 |
Swift 프로토콜 (Protocol) (0) | 2019.05.10 |