<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>kiwi.log</title>
        <link>https://velog.io/</link>
        <description>🐣 iOS Developer</description>
        <lastBuildDate>Wed, 01 May 2024 09:40:51 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>kiwi.log</title>
            <url>https://velog.velcdn.com/images/shisu_shin/profile/25e87fc2-67c1-4c83-8f1f-d667e97eb961/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. kiwi.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/shisu_shin" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[iOS] 영화예매앱 만들기[2] - 콜렉션 뷰와 페이지네이션]]></title>
            <link>https://velog.io/@shisu_shin/iOS-%EC%98%81%ED%99%94%EC%98%88%EB%A7%A4%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B02-%EC%BD%9C%EB%A0%89%EC%85%98-%EB%B7%B0%EC%99%80-%ED%8E%98%EC%9D%B4%EC%A7%80%EB%84%A4%EC%9D%B4%EC%85%98</link>
            <guid>https://velog.io/@shisu_shin/iOS-%EC%98%81%ED%99%94%EC%98%88%EB%A7%A4%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B02-%EC%BD%9C%EB%A0%89%EC%85%98-%EB%B7%B0%EC%99%80-%ED%8E%98%EC%9D%B4%EC%A7%80%EB%84%A4%EC%9D%B4%EC%85%98</guid>
            <pubDate>Wed, 01 May 2024 09:40:51 GMT</pubDate>
            <description><![CDATA[<h2 id="🌳-bodyview---콜렉션-뷰">🌳 BodyView - 콜렉션 뷰</h2>
<p>bodyView는 resultView에 collectionView를 쌓아서 완성했다!</p>
<pre><code class="language-swift">    let resultView: UIView = UIView()

    private let collectionViewFlowLayout: UICollectionViewFlowLayout = {
        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .vertical
        return layout
    }()

    lazy var cardContent: UICollectionView = {
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewFlowLayout)
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        collectionView.delegate = self
        collectionView.dataSource = self
        collectionView.register(SearchViewCell.self, forCellWithReuseIdentifier: &quot;SearchViewCell&quot;)
        collectionView.backgroundColor = .clear
        return collectionView
    }()</code></pre>
<p>UICollectionFlowLayout은 UICollectionView의 레이아웃을 관리하는 클래스로, 셀의 배치, 셀의 크기 및 간격 조정, 헤더 및 푸터 지정, 스크롤 방향 조절, 부분적인 레이아웃 업데이트를 지원한다. </p>
<p><br><br></p>
<h3 id="💢-셀의-이미지뷰에-cornerradius랑-shadow-동시에-적용하기">💢 셀의 이미지뷰에 cornerRadius랑 shadow 동시에 적용하기</h3>
<p>셀의 이미지뷰와 cornerRadius랑 shadow 효과를 동시에 적용하니까 안되었다!!😫😫😫</p>
<pre><code class="language-swift">    lazy var movieImage: UIImageView = {
        let imageView = UIImageView()
        imageView.contentMode = .scaleAspectFill
        imageView.layer.cornerRadius = 10
        imageView.clipsToBounds = true
        imageView.layer.shadowColor = UIColor.black.cgColor
        imageView.layer.shadowOpacity = 0.6
        imageView.layer.shadowOffset = CGSize(width: 0, height: 0)
        imageView.layer.shadowRadius = 5
        imageView.layer.masksToBounds = false
        imageView.layer.shouldRasterize = false
        return imageView
    }()</code></pre>
<p>위의 코드를 적용해서 실행하면 요렇게 나온다. cornerRadius가 적용 안된걸 확인할 수 있다.그리고 imageView의 사방에 그림자를 적용했는데 아래에만 적용된걸 볼 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/shisu_shin/post/a62ff5b1-8944-4e8c-ad31-922330852e51/image.png" alt=""></p>
<p>hierachy구조를 보면
<img src="https://velog.velcdn.com/images/shisu_shin/post/670c4aa3-20e8-4345-ba96-d425cb322b59/image.png" alt="">
옆에 인스펙터 창을 보면 *<em>clipsToBounds를 분명 true로 설정했는데 off 로 되어있다!!! *</em>
<br></p>
<h4 id="👩🏫-clipstobound--maskstobound-featcalayer">👩‍🏫 clipsToBound &amp;&amp; masksToBound (feat.CALayer)</h4>
<p>clipsToBounds 는 </p>
<ul>
<li>UIView의 프로퍼티</li>
<li>디폴트값은 false</li>
<li>true : superView 이외 영역의 Sub View는 Draw 하지 않는다</li>
<li>false : superView 이외 영역의 Sub View도 Draw 한다</li>
</ul>
<p>masksToBound 는 </p>
<ul>
<li>CALayer의 프로퍼티</li>
<li>디폴트값은 false</li>
<li>true : superLayer 이외 영역의 subLayer는 Draw 하지 않는다</li>
<li>false : superLayer 이외 영역의 subLayer도 Draw 한다</li>
</ul>
<p>참고로 CALayer란 
<a href="https://babbab2.tistory.com/53">CALayer</a> : 그림자, 그라데이션, 이미지, 텍스트 등 다양한 그래픽 콘텐츠를 표시하는 layer이다! (개발자소돌이님이 정말 잘 정리해주셨다!!)</p>
<p>즉, 두 메서드 모두 같은 속성을 건드리고 있는 것이다! 그래서 이에 대한 해결방안은? 이미지뷰 뒤에 같은 크기의 UIView를 두어 그 뷰에 그림자를 적용하는 것!(튜터님이 알려주셨다)</p>
<blockquote>
<p>⛔️ 문제 : 이미지뷰에 cornerRadius와 shadow를 둘다 적용하려니 clipsToBound 속성이 충돌한다!!
👩‍🏫 해결방법  : 뷰를 분리해서 하나의 뷰에는 cornerRadius를 다른 하나에 shadow를 적용해주자!</p>
</blockquote>
<p><a href="https://tong94.tistory.com/20">참고한 블로그</a>
<a href="https://babbab2.tistory.com/47">참고한 블로그</a></p>
<p><br><br></p>
<h3 id="💢-셀의-특정-indexpath만-reloaddata-하기">💢 셀의 특정 indexPath만 reloadData 하기</h3>
<p>api에서 page단위로 영화 정보를 받아오기 때문에 나는 페이지를 내리다가 마지막 셀을 willDisplay할때 loadSearchMore()를 실행해서 데이터를 받아와서 기존 데이터인 movieResult배열에 추가해주는 방식으로 코드를 작성했다. </p>
<pre><code class="language-swift">    func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
        if indexPath.row == movieResult.count - 1{
            if searchWord == &quot;&quot; {
                loadTrendingMore()
            } else {
                loadSearchMore()
            }
        }
    }</code></pre>
<p>여기서 loadTrendingMore()랑 loadSearchMore()은 불러오는 api 차이이고 구조는 같다. loadSearchMore()에서 특정 indexPath만 로드하기 위해 <code>self.cardContent.reloadData()</code> 대신 <code>self.cardContent.reconfigureItems(at: newIndexPaths)</code>를 사용했다.</p>
<pre><code class="language-swift">    func loadSearchMore() {
        if page &lt;= searchResult.totalPages! {
            page += 1
            networkManager.fetchSearchResult(page: page, searchKeyword: searchWord) {  [weak self] result in
                guard let self = self else { return }
                switch result {
                case .success(let response):
                    let startIndex = self.movieResult.count - 1 // 새로운 아이템의 시작 인덱스
                    let endIndex =  self.movieResult.count + response.results.count // 새로운 아이템의 끝 인덱스
                    self.movieResult += response.results
                    var newIndexPaths: [IndexPath] = []
                    for index in startIndex..&lt;endIndex {
                        let indexPath = IndexPath(row: index, section: 0)
                        newIndexPaths.append(indexPath)
                    }
                    DispatchQueue.main.async {
                        //self.cardContent.reloadData()
                        self.cardContent.reconfigureItems(at: newIndexPaths)
                    }
                case .failure(let error):
                    self.printError(error)
                }
            }
        }
    }</code></pre>
<p><br><br></p>
<h2 id="🌳-bodyview---uiview-as-image">🌳 BodyView - UIView as Image</h2>
<p>사진을 api에서 로드해오는 과정에서 이미지가 로딩중일때 로딩이미지를, 이미지가 없을때는 로고 이미지를 표시하기로 했다. 아래와 같이 로딩으로 등록된 에셋은 로딩 아이콘이다. 
<img src="https://velog.velcdn.com/images/shisu_shin/post/5b914454-e373-42bf-9d67-5df1deb43632/image.png" alt="">
로고 이미지는 로코 아이콘이 아니라 흰바탕에 로고가 있는 로고 이미지이다.
<img src="https://velog.velcdn.com/images/shisu_shin/post/1bb5d5c7-c77b-45e4-b696-0c4925b30f7d/image.png" alt="">
로딩과 같은 경우 그대로 이미지뷰에 넣어주면 이상한 모양이 나온다!
<img src="https://velog.velcdn.com/images/shisu_shin/post/6383cb34-db58-4d47-b241-543a868b3b99/image.gif" alt="">
로딩될때 살짝 확인할 수 있는데 안예쁘다!! 그래서 생각한 방법은 일단 로고를 정중앙에 배치한 뷰를 이미지화 해서 로딩이미지로 사용하는 것이다! 그래서 열심히 인터넷 검색을 통해 다음과 같은 extension을 찾았다.</p>
<pre><code class="language-swift">extension UIView {
  func asImage() -&gt; UIImage {
        let renderer = UIGraphicsImageRenderer(bounds: bounds)
        return renderer.image { rendererContext in
            layer.render(in: rendererContext.cgContext)
        }
    }
}</code></pre>
<p>이 asImage 메서드를 보자. UIGraphicsImageRenderer는 이미지를 렌더링 하기 위한 클래스로 UIView의 크기에 해당하는 렌더러를 생성한다. 그리고 리턴값에 있는 클로저는 UIView의 현재 내용을 이미지로 만든다고 생각하면 된다. 
<img src="https://velog.velcdn.com/images/shisu_shin/post/71efb7d5-33ae-497a-b9c4-f28416c25226/image.png" alt="">
로딩 이미지를 표시하는 방법은 텍스트의 플레이스 홀더를 지정하듯이 네트워크 매니저를 통해 이미지를 받기전에 미리 로딩이미지를 넣어주는 방식으로 진행하였다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[iOS] 영화예매앱 만들기[1] - 서치바 커스터마이징]]></title>
            <link>https://velog.io/@shisu_shin/iOS-%EC%98%81%ED%99%94%EC%98%88%EB%A7%A4%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B01-%EC%84%9C%EC%B9%98%EB%B0%94</link>
            <guid>https://velog.io/@shisu_shin/iOS-%EC%98%81%ED%99%94%EC%98%88%EB%A7%A4%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B01-%EC%84%9C%EC%B9%98%EB%B0%94</guid>
            <pubDate>Tue, 30 Apr 2024 09:35:07 GMT</pubDate>
            <description><![CDATA[<h2 id="📁-영화예매앱-만들기">📁 영화예매앱 만들기</h2>
<ul>
<li>프로젝트 주제 : 영화예매 앱을 만들자!</li>
<li>내가 맡은 기능 : 검색 기능과 탭바
<img src="https://velog.velcdn.com/images/shisu_shin/post/1b5aa7f8-5a7d-4c57-aa99-eaabe8ab7ad6/image.png" alt=""></li>
</ul>
<p><br><br></p>
<h3 id="🌳-view-구조-나누기">🌳 View 구조 나누기</h3>
<p>일단 내 뷰를 크게 top, body로 나누었다. top에는 서치바 있는 부분 SearchView가 들어가고, body에는 최신검색어가 나오는 부분인 LastSearchedView와 검색결과가 나오는 부분인 ResultView가 들어간다.
<img src="https://velog.velcdn.com/images/shisu_shin/post/e1df3a5d-20f9-4008-8689-61b6a8f9226b/image.png" alt=""></p>
<p><br><br></p>
<h3 id="🌳-topview---서치바-커스터마이징">🌳 TopView - 서치바 커스터마이징</h3>
<p>검색창을 보면 서치바를 사용하는 것 뿐만 아니라 커스텀까지 해야했다!!🙊🙉 </p>
<p>서치바를 사용하는 방법은 tableView와 비슷한다. UISearchBarDelegate를 지정해줘야한다. 
일단 먼저 서치바를 만들어주고 커스텀을 해준다. </p>
<pre><code class="language-swift">lazy var searchBar: UISearchBar = {
        let search = UISearchBar()
        search.placeholder = &quot;영화를 검색해보세요&quot;
        search.searchBarStyle = .minimal
        search.searchTextField.borderStyle = .none
        search.delegate = self
        if let tf = search.value(forKey: &quot;searchField&quot;) as? UITextField {
            if let leftView = tf.leftView as? UIImageView {
                leftView.tintColor = .megaRed
            }
        }
        return search
    }()</code></pre>
<blockquote>
<p>lazy var로 정의 : 초기화 시점을 지연시켜 메모리를 효율적으로 사용할 수 있다!! 이에 대해서는 나중에 꼼꼼히 정리해봐야겠음.</p>
</blockquote>
<p>여기서 searchBar는 여러 키값을 가지고 있는데 searchField는 검색 바의 텍스트 필드에 직접 접근할 수 있게 해준다. 이를 value(forKey:)로 받아온 다음 UITextField로 다운캐스팅 해주었다. 
<img src="https://velog.velcdn.com/images/shisu_shin/post/2ec3f544-e11f-4f1c-940f-c69d60ce8e77/image.png" alt=""></p>
<p>여기서 텍스트필드에는 검색창 뿐만 아니라 옆에 검색 버튼도 있는데 이 버튼도 UIImageView로 다운캐스팅 해와서 색을 바꾸어 주었다!!
<img src="https://velog.velcdn.com/images/shisu_shin/post/4db5c453-8a30-4f57-8290-2c2d8c4b3abe/image.png" alt="">
일단 요렇게 나온다. </p>
<blockquote>
<p>다운캐스팅 한번 더 정리!</p>
</blockquote>
<ul>
<li>안전한 다운캐스팅(as?): 다운캐스팅이 실패할 수 있는 경우에 사용. 다운캐스팅이 실패하면 nil을 반환.</li>
<li>강제 다운캐스팅(as!): 다운캐스팅이 확실히 성공할 것으로 확신할 때 사용. 만약 다운캐스팅이 실패하면 런타임 에러가 발생.</li>
</ul>
<p>여기서 나는 서치바 자체를 커스터마이징 하지 않고 뒤에 뷰를 하나 만들어주고 서치바를 투명하게 만들어주어서 마치 서치뷰가 동그란 것 처럼 했다!! 이 뷰에 그림자 효과도 주었다!!</p>
<pre><code class="language-swift">let searchView: UIView = {
        let bgview = UIView()
        bgview.layer.shadowColor = UIColor.black.cgColor
        bgview.layer.shadowOpacity = 0.6
        bgview.layer.shadowOffset = CGSize(width: 0, height: 0)
        bgview.layer.shadowRadius = 5
        bgview.layer.masksToBounds = false
        bgview.layer.shouldRasterize = false
        bgview.backgroundColor = UIColor.white
        bgview.layer.borderWidth = 1
        bgview.layer.borderColor = UIColor.clear.cgColor
        bgview.layer.cornerRadius = 20
        return bgview
    }()</code></pre>
<p>그런데 지금 정리하면서 서치바 자체를 커스텀 하는 것도 가능한 것을 발견! 왜 그때는 해볼 생각을 안했지? 🫤</p>
<pre><code class="language-swift">lazy var searchBar: UISearchBar = {
        let search = UISearchBar()
        search.placeholder = &quot;영화를 검색해보세요&quot;
        search.layer.cornerRadius = 20
        search.layer.borderColor = UIColor.black.cgColor
        search.layer.borderWidth = 1
        search.searchBarStyle = .minimal
        search.searchTextField.borderStyle = .none
        search.layer.shadowColor = UIColor.black.cgColor
        search.layer.shadowOpacity = 0.6
        search.layer.shadowOffset = CGSize(width: 0, height: 0)
        search.layer.shadowRadius = 8
        search.layer.masksToBounds = false
        search.layer.shouldRasterize = false
        search.delegate = self
        if let tf = search.value(forKey: &quot;searchField&quot;) as? UITextField {
            if let leftView = tf.leftView as? UIImageView {
                leftView.tintColor = .red
            }
        }
        return search
    }()
</code></pre>
<p>된다!
<img src="https://velog.velcdn.com/images/shisu_shin/post/f9fa7709-396b-43a2-a69d-6a384fbd7820/image.png" alt=""></p>
<p><br><br></p>
<h3 id="🌳-topview---uisearchbardelegate-설정하기">🌳 TopView - UISearchBarDelegate 설정하기</h3>
<p>UISearchBarDelegate를 설정해줘야 한다. 안해주면 그냥 textField와 다름이 없음.. 그래서 얘의 역할이 뭐냐... UITableViewDelegate랑 같다. &quot;UISearchBar 객체의 동작을 관리하기 위한 메서드와 속성을 정의&quot; 한다.</p>
<p>주요 메서드 속성은 다음과 같다. (fromGPT)</p>
<blockquote>
<ul>
<li>searchBarSearchButtonClicked(_:): 사용자가 검색 버튼을 눌렀을 때 호출됩니다. 이 메서드를 사용하여 검색을 실행하거나 관련 작업을 수행할 수 있습니다.</li>
</ul>
</blockquote>
<ul>
<li>searchBarCancelButtonClicked(_:): 사용자가 취소 버튼을 눌렀을 때 호출됩니다. 이 메서드를 사용하여 검색 작업을 취소하거나 관련 작업을 수행할 수 있습니다.</li>
<li>searchBar(_:textDidChange:): 사용자가 검색 바의 텍스트를 변경할 때 호출됩니다. 이 메서드를 사용하여 실시간 검색 결과를 업데이트하거나 관련 작업을 수행할 수 있습니다.</li>
<li>searchBar(_:selectedScopeButtonIndexDidChange:): 사용자가 검색 바의 범위 버튼(스코프 버튼)을 변경할 때 호출됩니다. 이 메서드를 사용하여 선택된 스코프에 따라 검색 동작을 조정하거나 관련 작업을 수행할 수 있습니다.</li>
<li>searchBarShouldBeginEditing(_:): 사용자가 검색 바를 편집하기를 시도할 때 호출됩니다. 이 메서드를 사용하여 검색 바를 편집할 수 있는지 여부를 결정할 수 있습니다.</li>
<li>searchBarTextDidBeginEditing(_:): 사용자가 검색 바를 편집하기 시작했을 때 호출됩니다. 이 메서드를 사용하여 편집 작업을 시작할 때 필요한 작업을 수행할 수 있습니다.</li>
</ul>
<p>여기서 나는 서치바의 텍스트가 바뀌었을 때 바로 반영해서 결과를 보여줄수 있게 searchBar(<em>:textDidChange:)와 서치바에서 서치를 눌렀을 때 최신검색어에 데이터를 저장하기 위해 searchBarSearchButtonClicked(</em> searchBar: UISearchBar), 그리고 취소 버튼을 클릭했을 때의 처리를 위한 searchBarCancelButtonClicked(_:)를 사용!
코드는 다음과 같다.</p>
<pre><code class="language-swift">extension SearchViewController: UISearchBarDelegate {
    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
        print(&quot;사용자가 입력한 텍스트: \(searchText)&quot;)
        searchWord = searchText
        if searchText == &quot;&quot; {
            fetchTrendingResults()
        } else {
            updateSearchResults()
        }

    }

    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        print(&quot;사용자가 엔터 키를 눌렀습니다.&quot;)
        searchBar.resignFirstResponder()
        if let keyword = searchBar.text {
            searchList.append(keyword)
            searchWord = keyword
            addLastSearch()
        }
        resultView.removeFromSuperview()
        haveLastSearchList()
        lastSearchedCollectionView.reloadData()
    }

    func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
        fetchTrendingResults()
        searchBar.text = &quot;&quot;
    }
}</code></pre>
<hr>
<p>참고한 문서
<a href="https://sueaty.tistory.com/164">https://sueaty.tistory.com/164</a>
<a href="https://fomaios.tistory.com/entry/%EC%84%9C%EC%B9%98%EB%B0%94-%EC%BB%A4%EC%8A%A4%ED%85%80%ED%95%98%EA%B8%B0-Custom-UISearchBar">https://fomaios.tistory.com/entry/%EC%84%9C%EC%B9%98%EB%B0%94-%EC%BB%A4%EC%8A%A4%ED%85%80%ED%95%98%EA%B8%B0-Custom-UISearchBar</a>
<a href="https://sangnam2.tistory.com/entry/Xcode-%EC%8B%9C%EB%AE%AC%EB%A0%88%EC%9D%B4%ED%84%B0-%EC%B6%94%EA%B0%80-iOS-%EB%B2%84%EC%A0%84-%EC%84%A0%ED%83%9D">https://sangnam2.tistory.com/entry/Xcode-%EC%8B%9C%EB%AE%AC%EB%A0%88%EC%9D%B4%ED%84%B0-%EC%B6%94%EA%B0%80-iOS-%EB%B2%84%EC%A0%84-%EC%84%A0%ED%83%9D</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[iOS] AppDelegate, SceneDelegate]]></title>
            <link>https://velog.io/@shisu_shin/iOS-AppDelegate-SceneDelegate</link>
            <guid>https://velog.io/@shisu_shin/iOS-AppDelegate-SceneDelegate</guid>
            <pubDate>Thu, 25 Apr 2024 03:03:21 GMT</pubDate>
            <description><![CDATA[<p>xcode에서 iosApp 프로젝트를 만들면 꼭 생성되는 두 녀석들이 있다. 바로 AppDelegate.swift와 SceneDelegate.swift! </p>
<h2 id="🌳-appdelegateswift">🌳 AppDelegate.swift</h2>
<p>이 파일은 앞에서 말했다싶이 iOS 앱의 전반적인 생명주기를 관리한다고 보면 된다! (<a href="https://velog.io/@shisu_shin/iOS-iOS-%EC%95%B1%EC%9D%98-%EC%83%9D%EB%AA%85-%EC%A3%BC%EA%B8%B0">여기 참고</a>)</p>
<h3 id="🌿-appdelegateswift의-주요-메서드">🌿 AppDelegate.swift의 주요 메서드:</h3>
<ul>
<li>application(_:willFinishLaunchingWithOptions:) : 앱이 구동되어 필요한 초기 실행 과정이 완료되기 직전에 호출</li>
<li>application(_:didFinishLaunchingWithOptions:) : 앱이 사용자에게 화면으로 표시되기 직전에 호출</li>
<li>applicationDidBecomeActive(_:) : 앱이 활성 상태로 전환된 직후에 호출</li>
<li>applicationDidEnterBackground(_:) : 앱이 백그라운드로 이동한 직후에 호출</li>
<li>applicationWillTerminate(_:) : 앱이 종료되기 직전에 호출</li>
</ul>
<p><br><br><br></p>
<h2 id="🌳-scenedelegateswift">🌳 SceneDelegate.swift</h2>
<p>SceneDelegate는 iOS 13부터 추가된 클래스로, iOS 앱의 멀티 태스킹과 다중 창 관련 작업을 처리한다. tableViewDelegate가 tableView의 동작과 사용자 인터렉션을 관리하는 것처럼 <span style="background-color:#FFF6B9">SceneDelegate는 Scene에 대해 윈도우와 뷰 컨트롤러를 관리한다.</span></p>
<p>여기서 잠깐?
Scene은 뭐고 윈도우랑 뷰컨트롤러가 정확히 뭔데?
<img src="https://velog.velcdn.com/images/shisu_shin/post/53fbceb2-1eb0-4c81-83d8-a39d25fb1513/image.png" alt=""></p>
<p><strong>윈도우</strong>는 iOS에서 디바이스의 스크린을 빈틈없이 채우기 위한 객체로 항상 유저 인터페이스의 최상위에 위치한다. 뷰의 일종이지만 직접 콘텐츠를 가지지는 않으며 콘테츠를 가진 뷰를 내부에 배치하여 화면에 출력하는 역할을 한다! 화면이 전환되도 이 윈도우 객체는 전환되지 않으면 내부에 배치한 뷰가 변경된다는점! 대부분의 iOSApp은 life time동안 하나의 window만 생성하고, 사용한다고 한다.</p>
<p><strong>윈도우신</strong>은 이러한 윈도우를 관리하고 . 앱이 다중 창 환경에서 실행될 때 각 창마다 별도의 UIWindowScene이 필요(아이패드에서 분할화면 띄우는 것과 같이!)</p>
<p><strong>뷰</strong>는 화면의 콘텐츠를 말한다. 하나의 윈도우에 여러 뷰가 각자의 위치에 놓여있을것이다. 한 화면에 topView, bottomView 등등이 각자의 위치에서 각자의 역할을 하고 있는것! </p>
<p><strong>뷰 컨트롤러</strong>는 윈도우와 뷰를 연결해주는 객체이다. 뷰컨은 뷰의 계층을 관리하여 윈도우에 전달하고, 사용자 인터렉션을 윈도우로부터 전달받아 처리하는 역할을 한다. 윈도우와 뷰사이를 중계해주는것! 
<img src="https://velog.velcdn.com/images/shisu_shin/post/8b1675fa-1178-44e3-bd68-d222c177eb3d/image.png" alt="">
대략적으로 구조는 이러한데, UIWindowScene이 여러 UIWindow를 갖는 것은 각 창이 다른 UIWindow에 표시될 때 발생하며, UIScreen이 여러 UIWindowScene을 갖는 것은 iOS 앱이 여러 창을 지원할 때 발생한다.
<img src="https://velog.velcdn.com/images/shisu_shin/post/105a8df5-453a-47f1-bb4c-bd131c60bb18/image.png" alt="">
윈도우는 하나의 뷰컨을 루트뷰건으로 지정하여 참조한다. 루트 뷰 컨트롤러로 지정되지 못한 나머지 뷰 컨트롤러들은 루트 뷰 컨트롤러의 관리 대상으로 연결되거나 혹은 다른 방식으로 이어지기도 하지만，이들은 윈도우 객체의 직접적인 관리대상이 아니다.. 윈도우 객체는 항상 루트 뷰 컨트롤러만을 참조한다.</p>
<h3 id="🌿-scenedelegateswift의-주요-메서드">🌿 SceneDelegate.swift의 주요 메서드:</h3>
<p>scene(<em>:willConnectTo:options:): 새로운 Scene이 생성되고 윈도우가 연결될 때 호출. 여기서 Scene에 대한 초기 설정을 수행
sceneDidDisconnect(</em>:): Scene이 시스템에서 분리될 때 호출
sceneWillEnterForeground(<em>:): Scene이 포그라운드로 진입하기 직전에 호출
sceneDidEnterBackground(</em>:): Scene이 백그라운드로 들어갈 때 호출
sceneDidBecomeActive(<em>:): Scene이 활성 상태가 되었을 때 호출.
sceneWillResignActive(</em>:): Scene이 비활성 상태로 전환되기 직전에 호출</p>
<p><br><br><br></p>
<hr>
<p>참고
<a href="https://developer.apple.com/documentation/uikit/view_controllers/managing_content_in_your_app_s_windows?ref=noah-ios.dev">https://developer.apple.com/documentation/uikit/view_controllers/managing_content_in_your_app_s_windows?ref=noah-ios.dev</a>
<a href="https://noah-ios.dev/window-scene/">https://noah-ios.dev/window-scene/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[iOS] iOS 앱의 생명 주기]]></title>
            <link>https://velog.io/@shisu_shin/iOS-iOS-%EC%95%B1%EC%9D%98-%EC%83%9D%EB%AA%85-%EC%A3%BC%EA%B8%B0</link>
            <guid>https://velog.io/@shisu_shin/iOS-iOS-%EC%95%B1%EC%9D%98-%EC%83%9D%EB%AA%85-%EC%A3%BC%EA%B8%B0</guid>
            <pubDate>Wed, 24 Apr 2024 15:57:09 GMT</pubDate>
            <description><![CDATA[<h2 id="📖-앱의-초기화-과정">📖 앱의 초기화 과정</h2>
<p>c언어와 마찬가지로 iOS 앱도 main()함수로 부터 시작한다!</p>
<p><img src="https://velog.velcdn.com/images/shisu_shin/post/532b4926-8790-4df8-8455-aed9f0cf7681/image.png" alt=""></p>
<h4 id="1-user-taps-application-icon">1. User taps application icon</h4>
<h4 id="2-main">2. main()</h4>
<p>Xcode에서 프로젝트를 생성하면 main() 함수가 자동으로 만들어지며, 여기에는 앱이 실행될 때 처리해야할 내용이 작성되어 있다. (그래서 개발자가 딱히 신경쓸 필요가 없음!) iOS 앱에서 main()함수가 하는 일은 실행 시 시스템으로부터 전달받은 두 개의 인자값과 <strong>AppDelegate 클래스</strong>를 이용하여 UIApplicationMain( ) 함수를 호출하고, 그 결과로 UIApplication 객체를 반환한다. </p>
<h4 id="3-uiapplicationmain">3. UIApplicationMain()</h4>
<p>이때 호출되는 <span style="background-color:#FFF6B9"><strong>UIApplicationMain()이 iOS앱의 엔트리 포인트</strong></span>이다. 이 함수는 앱의 핵심 객체를 생성하는 프로세스를 핸들링하고，스토리보드 파일로부터 앱의 유저 인터페이스를 읽어 들일뿐만 아니라 우리가 작성한 커스텀 코드를 호출해 줌으로써 앱 생성 초기에 필요한 설정을 구현할 수 있게 해 준다. 사실상 거의 앱 실행에 필요한 전반적인 것을 관리한다고 생각하면 된다. 즉, <strong>UIApplication 객체 == 앱 프로세스!</strong>
UlApplication 객체는 Info.plist 파일을 바탕으로 앱에 필요한 데이터와 객체를 로드한다.</p>
<h4 id="4-applicationdidfinishlaunching">4. applicationDidFinishLaunching</h4>
<p><span style="background-color:#FFF6B9">AppDelegate는 UIApplication으로부터 위임받은 일부 권한을 이용하여 커스텀 코드와 상호작용하는 역할을 담당하고，이를 통해 우리가 필요한 코드를 구현할 수 있도록 도와준다</span>. 이 메소드에 원하는 커스텀 코드를 작성해 두면 앱이 처음 시작될 때 해당 코드를 실행할 수 있다! 런치스크린 설정할때 이 메서드에 sleep()을 넣은 이유가 이거였다!!!!!!!</p>
<h4 id="5-event-loop---handle-event">5. event loop &lt;-&gt; handle event</h4>
<h4 id="6-system-asks-application-to-terminate---applicationwillterminate">6. system asks application to terminate &lt;-&gt; applicationWillTerminate</h4>
<p>앱이 더 이상 사용되지 않는다면 시스템은 앱을 메모리에서 제거할려고 할 것이다. 이 때 AppDelegate 클래스의 applicationWillTerminate(_:) 메소드를 호출해 종료시 실행된다. </p>
<p><br><br><br></p>
<h2 id="📖-앱의-상태-변화">📖 앱의 상태 변화</h2>
<p>앱의 상태란? 앱이 실행되는 동안 변하는 모습?이라고 할 수 있다. (활성, 비활성 등등) <span style="background-color:#FFF6B9">앱의 상태 변화는 운영체제가 처리하는데 iOS는 시스템에서 발생하는 특정 상황에 맞게 앱의 상태를 변화시키고 제어한다.</span> 
예를 들어 애플뮤직을 실행하다가 메세지 앱으로 화면 전환을 해서 메세지를 작성해도 계속 음악이 흘러나온다. 이때 운영체제가 애플뮤직 앱의 상태를 변경시켜주는 것이다.</p>
<ul>
<li><strong>Not Running</strong> : 앱이 시작되지 않았거나 실행되었지만 시스템에 의해 종료된 상태</li>
<li><strong>Active(활성)</strong> : 앱이 실행중이며, 이벤트를 받고 있는 상태</li>
<li><strong>Inactive(비활성)</strong> :  앱이 현재 활성이지만, 사용자의 상호 작용을 받지 않는 상태(이벤트X)</li>
<li><strong>Background(백그라운드)</strong> : 앱이 백그라운드에서 실행되고 있지만, 사용자와의 상호 작용이 없는 상태</li>
<li><strong>Suspended(중단)</strong> : 앱이 백그라운드에 있지만 실행 중이 아닌 상태.  메모리가 부족한 상황이 오면 iOS 시스템은 포그라운드에 있는 앱의 여유 메모리 공간을 확보하기 위해 Suspended 상태에 있는 앱들을 특별한 알림 없이 정리한다.</li>
</ul>
<p><br><br><br></p>
<h2 id="📖-앱의-생명주기">📖 앱의 생명주기</h2>
<p><img src="https://velog.velcdn.com/images/shisu_shin/post/cb94908c-7a72-4252-820a-8685d9ea1f6d/image.png" alt="">
그림과 같이 앱은 다음과 같은 순서로 실행되고, 실행 상태가 변화된다. <strong><span style="color:FireBrick">앱의 실행 상태가 변화할 때 그에 대응하는 앱 델리게이트에 정의된 특정 메소드를 호출한다.</span></strong></p>
<ul>
<li>application(_:willFinishLaunchingWithOptions:) : 앱이 구동되어 필요한 초기 실행 과정이 완료되기 직전에 호출</li>
<li>application(_:didFinishLaunchingWithOptions:) : 앱이 사용자에게 화면으로 표시되기 직전에 호출</li>
<li>applicationDidBecomeActive(_:) : 앱이 활성 상태로 전환된 직후에 호출</li>
<li>applicationDidEnterBackground(_:) : 앱이 백그라운드로 이동한 직후에 호출</li>
<li>applicationWillTerminate(_:) : 앱이 종료되기 직전에 호출</li>
</ul>
<br>

<hr>
<p>참고
<a href="https://developer.apple.com/documentation/uikit/uiapplicationdelegate">https://developer.apple.com/documentation/uikit/uiapplicationdelegate</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[iOS] 런치스크린 (시작화면) 설정하기]]></title>
            <link>https://velog.io/@shisu_shin/iOS-%EB%9F%B0%EC%B9%98%EC%8A%A4%ED%81%AC%EB%A6%B0-%EC%8B%9C%EC%9E%91%ED%99%94%EB%A9%B4-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@shisu_shin/iOS-%EB%9F%B0%EC%B9%98%EC%8A%A4%ED%81%AC%EB%A6%B0-%EC%8B%9C%EC%9E%91%ED%99%94%EB%A9%B4-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 24 Apr 2024 14:42:59 GMT</pubDate>
            <description><![CDATA[<h2 id="📖-런치스크린">📖 런치스크린?</h2>
<p>가지고 있는 아무 앱이나 실행시켜보면 본격적인 앱이 시작되기 전에 로고가 나오는 화면이 몇초동안 실행되는 것을 볼 수 있다. 이를 런치스크린(launch Screen)이라고 한다. 다른 말로는 splash라고도 한다.
<img src="https://velog.velcdn.com/images/shisu_shin/post/88e45af6-8fb3-4e6b-bf74-c1deeb180c3f/image.png" alt=""></p>
<p>런치스크린이 큰 의미가 없어 보이지만 앱이 처음 실행될 때 주요 데이터를 초기화할 수 있는 시간을 벌어주는 역할을 한다! 사용자에게 자연스러운 대기시간을 만들어주는것!! 애플의 UI가이드에서도 런치스크린 제작을 권장하고 있다.</p>
<h2 id="📖-런치스크린-설정하기">📖 런치스크린 설정하기</h2>
<p>ios에서 프로젝트를 만들면 시작화면파일이 제공된다. 파일 이름은 <strong>LaunchScreen.storyboard</strong>로 스토리보드 형식의 파일이다. 
<img src="https://velog.velcdn.com/images/shisu_shin/post/6116e428-7b7a-4bf4-89a5-884de98ddb59/image.png" alt="">
main.storyboard처럼 사용자가 원하는대로 라벨과 이미지를 사용해 꾸밀 수 있다. 그리고 설정항목에서 Launch Screen File에 Launch Screen을 설정할 수 있다. 나의 경우 LaunchScreen.storyboard로 지정해주었다. 
<img src="https://velog.velcdn.com/images/shisu_shin/post/4b2e035c-b868-4c41-8709-aab5250d9469/image.png" alt="">
이렇게하면 런치스크린 설정이 완료된 것이다!! 이제 런치스크린이 나올 시간을 정해보자. AppDelegate 클래스에서 설정할 수 있다. <code>func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -&gt; Bool</code>은 앱이 처음 실행될 때，필요한 시스템적 처리를 모두 끝내고 메인 화면을 표시하기 직전에 호출된다. 이 메소드에 시간지연을 하는 코드를 추가해서 런치스크린이 나오는 시간을 추가할 수 있다. 나는 3초 sleep하게 해서 지연시켰다.
<img src="https://velog.velcdn.com/images/shisu_shin/post/d5e554e4-b7a7-4ad1-a8ef-fecd6b279652/image.png" alt=""></p>
<blockquote>
<p>AppDelegate 클래스는 앱 전체의 실행 흐름을 컨트롤하는 객체로서 앱이 처음 실행 되거나 종료될 때，혹은 백그라운드 상태로 들어가거나 포그라운드 상태로 활성화될 때 호출되는 메소드들로구성되어 있다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/shisu_shin/post/961a7780-5f00-4967-ade0-b35c631832e9/image.gif" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[문풀] str.replacingOccurrences]]></title>
            <link>https://velog.io/@shisu_shin/%EB%AC%B8%ED%92%80-str.replacingOccurrences</link>
            <guid>https://velog.io/@shisu_shin/%EB%AC%B8%ED%92%80-str.replacingOccurrences</guid>
            <pubDate>Fri, 19 Apr 2024 10:23:43 GMT</pubDate>
            <description><![CDATA[<h3 id="⚙️-숫자-문자열과-영단어">⚙️ 숫자 문자열과 영단어</h3>
<p><img src="https://velog.velcdn.com/images/shisu_shin/post/d0b5657b-e3f7-449e-9165-3396fe2d20b9/image.png" alt=""></p>
<p>음 일단 보자마자 딕셔너리!! 한번 써봐야 겠다. 문제 풀면서 딕셔너리를 사용한 적이 한번도 없는것 같아 이번 기회에 써야겠다는 생각을 했다. 그래서 딕셔너리를 사용해서 어떻게 풀까하다 생각하고 코드를 적었다.</p>
<pre><code class="language-swift">import Foundation

func solution(_ s:String) -&gt; Int {
    let dic = [&quot;one&quot;: 1, &quot;two&quot;: 2, &quot;three&quot;: 3, &quot;four&quot;: 4,
                              &quot;five&quot;: 5, &quot;six&quot;: 6, &quot;seven&quot;: 7, &quot;eight&quot;: 8,
                              &quot;nine&quot;: 9, &quot;zero&quot;: 0]
    var result: [Int] = []
    var flag: String = &quot;&quot;
    for char in s {
        if let temp = Int(String(char)) {
            result.append(temp)
        } else {
            flag.append(char)
            if dic.keys.contains(flag) {
                result.append(dic[flag]!)
                flag = &quot;&quot;
            }
        }
    }
    let res = result.map { String($0) }.joined()
    return Int(res)!
}</code></pre>
<p>이게 내가 제출한 정답 코드인데 사실 나는 문제를 풀때 플레이그라운드 파일에서 푼다! 실행한 결과를 줄바줄로 바로바로 볼수있기 때문! 근데 장점이라고 해야할지 단점이라고 해야할지 여기서 자동완성기능과 더불어 객체에 속한 프로퍼티랑 메서드를 볼 수 있다. ㅎㅎ 그래서 코드의 <code>dic.keys.contains(flag)</code> 이 부분도 사실 &#39;Swift라면 딕셔너리에 이런 기능정도는 제공해주지 않을까~~&#39;라는 생각으로 .찍고 찾은거다...ㅎㅎㅎㅎㅎㅎ</p>
<p>그리고 앱을 만들때는 강제언래핑이 컴파일 에러를 발생시킬 수 있어서 보통 옵셔널 바인딩으로 많이 처리하는데 이렇게 !를 자주 사용해도 되나 싶다.. 알고리즘 문제는 정해진 입력값이 있다보니 그걸 이용해서 그냥 믿고? 강제언래핑하는데 이걸 옵셔널바인딩으로 바꿔서 풀어야 하나? </p>
<p>다른사람들의 풀이를 보니 역시 기깔나는 string 객체 메서드가 있었다!</p>
<pre><code class="language-swift">import Foundation

func solution(_ s:String) -&gt; Int {
    let arr = [&quot;zero&quot;,&quot;one&quot;,&quot;two&quot;,&quot;three&quot;,&quot;four&quot;,&quot;five&quot;,&quot;six&quot;,&quot;seven&quot;,&quot;eight&quot;,&quot;nine&quot;]
    var str = s
    for i in 0..&lt;arr.count {
        str = str.replacingOccurrences(of: arr[i], with: String(i))
    }
    return Int(str)!
}</code></pre>
<p>와웅...👏👏👏👏👏👏👏 arr의 인덱스와 숫자를 페어로 사용했다!! 그리고 replacingOccurrences를 사용해서 string -&gt; Int할 때 인덱스를 사용함! </p>
<p><br><br></p>
<h3 id="📝-strreplacingoccurrencesof--with-">📝 str.replacingOccurrences(of: , with: )</h3>
<pre><code class="language-swift">func replacingOccurrences(
    of target: String,
    with replacement: String
) -&gt; String</code></pre>
<p>문자열에서 특정 부분 문자열을 다른 문자열로 대체하는 메서드이다. 이 메서드는 대체할 문자열이 발생한 횟수에 상관없이 모든 발생을 대체한다고 한다. 
<a href="https://developer.apple.com/documentation/foundation/nsstring/1412937-replacingoccurrences">developer.apple.com/documentation/foundation/nsstring</a></p>
<p><br><br><br></p>
<p>문제를 풀면서 느끼는게 사람이 자주쓰는것만 쓰는것 같다..(이게 바로 관성..?!?!)그래서 안쓰는 놈들을 정말 빨리 까먹음.. swift언어가 꽤 고급언어인지라 지원하는 기능이 정말정말 많은데 그걸 다 못 사용하고 있다는 생각이 든다. 이럴 땐 c언어로 코테를 보고 싶어진당 큽 학교에서 자료구조 강의 들을때만 해도 c랑 교수님을 엄청 미워했는데 이런 생각이 들다니...🙈🙉🙈🙉🙊🙊🙊 그냥 닥치고 해야겠당ㅎㅎ</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[iOS] 위시리스트 만들기 [2]]]></title>
            <link>https://velog.io/@shisu_shin/iOS-%EC%9C%84%EC%8B%9C%EB%A6%AC%EC%8A%A4%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0-2</link>
            <guid>https://velog.io/@shisu_shin/iOS-%EC%9C%84%EC%8B%9C%EB%A6%AC%EC%8A%A4%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0-2</guid>
            <pubDate>Fri, 19 Apr 2024 08:18:04 GMT</pubDate>
            <description><![CDATA[<h2 id="🌳-위시리스트-셀-삭제하기">🌳 위시리스트 셀 삭제하기</h2>
<p>위시리스트의 셀 삭제는 일단 익숙한 방법인 밀어서 삭제로 구현해보기로 했다! 일단 셀을 삭제하는 동시에 coredata에서 해당 내용도 삭제해주어야 한다. </p>
<pre><code class="language-swift">    func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -&gt; UITableViewCell.EditingStyle {
        return .delete
    }

    func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        if editingStyle == .delete {
            wishListTableView.beginUpdates()

            //1. coredata에서 데이터 삭제
            let object = self.dataList[indexPath.row]
            let appDelegate = UIApplication.shared.delegate as! AppDelegate
            let context = appDelegate.persistentContainer.viewContext
            context.delete(object)
            do {
                try context.save()
               } catch {
                   context.rollback()
            }

            //2. dataList에서 데이터삭제 및 테이블뷰셀 삭제
            self.dataList.remove(at: indexPath.row)
            wishListTableView.deleteRows(at: [indexPath], with: .fade)

            wishListTableView.endUpdates()
        }
    }</code></pre>
<p>여기서 <code>wishListTableView.beginUpdates()</code>랑 <code>wishListTableView.endUpdates()</code>를 안적으면 에러가 난다!! <strong>beginUpdates()를 호출하면 이후의 모든 삽입, 삭제 또는 이동 작업이 테이블 뷰에 일시적으로 대기 상태로 유지된다. 그런 다음 endUpdates()를 호출하면 대기 중인 모든 변경 사항이 한 번에 적용되어 테이블 뷰가 업데이트된다.</strong></p>
<p><a href="https://gonslab.tistory.com/45">테이블셀 스와이프 설정 바꾸기</a></p>
<h2 id="🌳-위시리스트-pull-to-refresh">🌳 위시리스트 Pull to Refresh</h2>
<p>pull to refresh는 깃허브 프로필 과제에서도 구현해본 적이 있었다. 그때 endRefreshing()을 하지않아서 로딩이 계속걸리는 문제가 있었는데 팀원분이 알려주셔서 해결했다!!</p>
<pre><code class="language-swift">wishListTableView.refreshControl = refreshControll
refreshControll.addTarget(self, action: #selector(self.refreshFunction), for: .valueChanged)

@objc func refreshFunction() {
    wishListTableView.reloadData()
    refreshControll.endRefreshing()
}</code></pre>
<p>여기서 <code>refreshControll.endRefreshing()</code>를 꼭 추가해주어야 한다! 아니면 계속 로딩 돌아감..</p>
<h2 id="🌳-완성본">🌳 완성본</h2>
<p><img src="https://velog.velcdn.com/images/shisu_shin/post/eee29e2c-12d4-4339-ae99-ce94a7fe5ce5/image.gif" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[iOS] 위시리스트 만들기 [1]]]></title>
            <link>https://velog.io/@shisu_shin/iOS-%EC%9C%84%EC%8B%9C%EB%A6%AC%EC%8A%A4%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0-1</link>
            <guid>https://velog.io/@shisu_shin/iOS-%EC%9C%84%EC%8B%9C%EB%A6%AC%EC%8A%A4%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0-1</guid>
            <pubDate>Thu, 18 Apr 2024 11:28:01 GMT</pubDate>
            <description><![CDATA[<p>위시리스트 화면을 제작해야한다. 대충 UI 가이드는 주어졌으며 필수 사항은 다음과 같다.</p>
<ul>
<li>URLsession을 이용해 네트워크 통신하기</li>
<li>core data를 사용해서 데이터 저장하기</li>
<li>화면전환으로 위시리스트 보여주기</li>
</ul>
<br>



<h2 id="🌳-urlsession-사용해서-네트워크-연결-및-사진불러오기">🌳 URLSession 사용해서 네트워크 연결 및 사진불러오기</h2>
<p>여태까지 네트워크 연결은 Alamofire, 사진 불러올 때는 KingFisher 등의 외부 라이브러리만 써서 xcode에서 제공하는 것만으로 네트워크 연결이랑 사진 불러오기를 해본적이 없다...ㅎㅎ😅 그래서 이번에 URLSession을 사용해서 네트워크 연결을 해보고 불러온 URL로 사진도 불러와 보았다.(사실 열심히 구글링 했다!)</p>
<p>먼저 URLSession 인스턴스를 생성하고</p>
<pre><code class="language-swift">let session = URLSession.shared</code></pre>
<p>url 상수에 URL 정보를 바인딩한다. 그리고 session의 dataTask 메서드로 URL 요청을 생성한다. 클로저는 요청이 완료되면 실행되며 여기서는 data, response, error를 받아와서 처리한다. </p>
<pre><code class="language-swift"> if let url = URL(string: &quot;https://dummyjson.com/products/\(productID)&quot;){
            let task = session.dataTask(with: url) { (data, response, error) in
                if let error = error {
                    print(&quot;Error: \(error)&quot;)
                } else if let data = data {
                    print(data)
                }
            }
            task.resume()
        }
    }</code></pre>
<p>이게 정상 실행되서 네트워크 연결이 되면 data에 응답이 들어온다. 그런데 이 응답은 api에 따라 다르겠지만 저 api는 JSON을 반환한다. 그래서 이 JSON을 미리 만들어준 RemoteProduct 구조체 객체로 디코딩 하기 위해 다음과 같은 코드를 적어줘야 한다.</p>
<pre><code class="language-swift">do {
    let product = try JSONDecoder().decode(RemoteProduct.self, from: data)
} catch {
    print(&quot;디코딩 에러: \(error)&quot;)
}</code></pre>
<p>요러면 네트워크 연결 완료!!
사진도 같은 맥락으로 불러오면 된다!</p>
<pre><code class="language-swift">if let imageURL = URL(string: product.thumbnail) {
    URLSession.shared.dataTask(with: imageURL){ (imageData, _, _) in
        if let imageData = imageData {
            // 이미지 데이터를 UIImage로 변환하여 UIImageView에 설정
            DispatchQueue.main.async {
                self.imageView.image = UIImage(data: imageData)
            }
        }
    }.resume()
}</code></pre>
<p><br><br></p>
<h2 id="🌳-데이터-모델링하기">🌳 데이터 모델링하기</h2>
<p>위시리스트의 데이터들은 코어데이터로 저장해야한다. 코어데이터를 추가하는 방법은 저번에 정리했었기 때문에 뚝딱 만들어주었다.
<img src="https://velog.velcdn.com/images/shisu_shin/post/af8957fb-38b0-4574-81e1-d93adbd758ca/image.png" alt="">
그런데 데이터를 불러오는 과정에서 에러가 났었다!!
<img src="https://velog.velcdn.com/images/shisu_shin/post/97a97398-fddf-4b47-ae3d-1a02087beece/image.png" alt="">
검색해보니 iOS 앱은 기본적으로 메인 스레드에서 UI 업데이트 및 상호 작용이 이루어져야 한다고 한다! 그런데 지금 이 do-catch 구문은 fetchData() 메서드의 일부분으로 <span style="color:FireBrick"><strong>이 메서드는 백그라운드 스레드에서 비동기적으로 실행된다.</strong></span> 그래서 UI를 직접 업데이트 하고 있는 코드들은 메인스레드로 이동시켜주어야 한다. 이를 위해 <span style="color:CornflowerBlue">DispatchQueue.main.async를 사용하여 메인 스레드에서 UI 업데이트를 수행하도록 변경</span>하여 문제를 해결할 수 있었다.</p>
<p><img src="https://velog.velcdn.com/images/shisu_shin/post/afd8b9fa-b7fe-47fb-be8e-1d446da1bdf6/image.png" alt=""></p>
<blockquote>
<ul>
<li>DispatchQueue.main.async : DispatchQueue에서 작업을 비동기적으로 실행. 주로 UI 업데이트와 같은 비동기적인 작업을 처리할 때 사용</li>
</ul>
</blockquote>
<ul>
<li>DispatchQueue.main.sync : DispatchQueue에서 작업을 동기적으로 실행. 주로 백그라운드 스레드에서 메인 스레드로 작업을 전환할 때 사용</li>
</ul>
<p><br><br></p>
<h2 id="🌳-코어데이터에서-조회한-정보를-셀에-뿌리기">🌳 코어데이터에서 조회한 정보를 셀에 뿌리기</h2>
<p>어째저째 흘러가다가 코어데이터에서 정보를 조회할때 에러가 발생했다. 정확히 말해서는 에러가 발생했다기 보다 정보가 조회가 안되었다! 아래와 같이 위시리스트에 상품정보만 뜨고 아이디랑 가격이 안뜨는 문제가 발생했다!!!!!!!!!!!!🙉</p>
<p>그래서 cellForRowAt 메서드에 print문을 찍어서 받아오는 title, total(id임, 네이밍 잘못함), price를 확인해봤다.</p>
<pre><code class="language-swift">    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -&gt; UITableViewCell {
        let record = self.dataList[indexPath.row]
        let title = record.value(forKey: &quot;title&quot;) as? String
        let total = record.value(forKey: &quot;id&quot;) as? String
        let price = record.value(forKey: &quot;price&quot;) as? String

        print(title, total, price)

        let cell = wishListTableView.dequeueReusableCell(withIdentifier: &quot;wishListTableViewCell&quot;, for: indexPath) as! wishListTableViewCell
        cell.cellTitleLabel.text = title
        cell.cellPriceLabel.text = price
        cell.cellTotalLabel.text = total

        return cell
    }</code></pre>
<p>실행하니 요래 나왔다..엥 왠 nil?? 뜬금없다. 일단 코어데이터에 저장하는 부분은 확인했다. 정상 저장된다. 즉 불러오는 과정이 문제라는 것이다.
<img src="https://velog.velcdn.com/images/shisu_shin/post/277b7b28-c358-4d89-bc04-b866e4b34b5e/image.png" alt=""></p>
<p>그래서 검색해보니 <span style="color:DarkOrange"><strong>value(forKey:) 메서드를 사용하여 값을 가져올 때 반환되는 값의 타입이 Any?</strong></span>이라고 한다. id 랑 price에는 Int 타입 정보가 들어있는데 나는 Int -&gt; String 변환은 항상 가능하기 때문에 as? String으로 타입을 변환했는데 이러면 안되고 as? Int로 변환해주어야한다고 한다. 그래서 고친 코드는 다음과 같다.</p>
<pre><code class="language-swift">let title = record.value(forKey: &quot;title&quot;) as? String
let total = record.value(forKey: &quot;id&quot;) as? Int
let price = record.value(forKey: &quot;price&quot;) as? Int</code></pre>
<p>any타입을 잘 몰라서 발생하는 실수였다. 정리하자면</p>
<blockquote>
<ul>
<li>any : 어떤 종류의 값이든 할당할 수 있는 유연한 타입이다. Int를 넣었다가 String 넣는것 가능!</li>
</ul>
</blockquote>
<ul>
<li>as? : 값의 타입을 지정된 타입으로 변환하고, 변환이 실패할 경우 nil을 반환</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[iOS] CoreData 사용해보기]]></title>
            <link>https://velog.io/@shisu_shin/iOS-CoreData</link>
            <guid>https://velog.io/@shisu_shin/iOS-CoreData</guid>
            <pubDate>Wed, 17 Apr 2024 10:09:00 GMT</pubDate>
            <description><![CDATA[<p>UserDefault는 사용해본 적이 있는데 이 친구는 처음이다..!!UserDefault보다 훨씬더 많은 내용을 저장할 수 있다는데 일단 시작해봄시다...👩‍🚀</p>
<h1 id="📖-coredata">📖 CoreData?</h1>
<h4 id="span-stylebackground-colorfff6b9ios-앱에서-데이터를-관리하고-저장하기-위한-프레임워크span"><span style="background-color:#FFF6B9">&quot;iOS 앱에서 데이터를 관리하고 저장하기 위한 프레임워크&quot;</span></h4>
<p>core data가 iOS앱에서 데이터를 관리하고 저장하는 도구라면, Core Data stack은 데이터를 관리하는 구조이다. core data stack이라는 구조로 core data는 데이터를 관리한다! 라고 생각하면 될듯!!!
(갑자기 막 새로운 내용이 머리에 들어와서 쪼금 혼란스럽다..🙉🙈🙉) </p>
<p><img src="https://velog.velcdn.com/images/shisu_shin/post/2e3b2bfa-298f-435b-992d-6ab2020d9372/image.png" alt=""></p>
<ul>
<li><strong>NSManagedObjectModel 관리 객체 모델</strong> : 데이터베이스에서 테이블의 구조를 정의하는 스키마에 해당하는 것으로, 코어 데이터에서 테이블에 대으오디는 엔티티의 구조를 정의하는 객체를 말한다.</li>
<li><strong>NSManagedObjectContext 관리 객체 컨텍스트</strong> : 관리 객체를 생성, 수정, 삭제하거나 메모리에 로드하는 작업을 제공한다. </li>
<li><strong>NSPersistentStoreCoordinator 영구 저장소 코디네이터</strong> : 영구 저장소를 관리하고 데이터베이스와의 통신을 담당하는 객체이다. 영구저장소와 관리 객체 모델을 연결한다.</li>
<li><strong>NSPersistentStore 영구 저장소</strong> : 실제로 데이터가 영구적으로 저장되는 저장소를 말한다.</li>
</ul>
<p><br><br></p>
<h1 id="coredata-생성하는법">CoreData 생성하는법</h1>
<h3 id="1-기존에-생성한-프로젝트에서-command--n-키를-눌러-datamodel-파일을-추가해준다">1. 기존에 생성한 프로젝트에서 command + N 키를 눌러 DataModel 파일을 추가해준다.</h3>
<p><img src="https://velog.velcdn.com/images/shisu_shin/post/baafe34b-db53-4ae7-ad84-6d63ad71ce8d/image.png" alt="">
그러면 이렇게 파일이 생성된걸 확인할 수 있다.
<img src="https://velog.velcdn.com/images/shisu_shin/post/33ebe646-17b4-4a60-9646-29ad4f860147/image.png" alt=""></p>
<br>

<h3 id="2-add-entity로-entity를-추가해준다-entity는-데이터베이스의-테이블과-비슷한-개념으로-데이터-모델-내에서-객체의-유형을-정의하고-해당-객체들이-갖는-속성을-나타낸다-나는-상품을-받기-위해-product라는-entity를-만들어주었다">2. Add Entity로 entity를 추가해준다. entity는 데이터베이스의 테이블과 비슷한 개념으로 데이터 모델 내에서 객체의 유형을 정의하고 해당 객체들이 갖는 속성을 나타낸다. 나는 상품을 받기 위해 Product라는 Entity를 만들어주었다.</h3>
<p>그리고 product Entity에 attribute를 추가해주었다. 
<img src="https://velog.velcdn.com/images/shisu_shin/post/047fbc12-7a16-413a-b239-85332e45926e/image.png" alt=""></p>
<blockquote>
<p>👩‍🏫 entity는 데이터 모델에서 개별 객체를 나타내며, attribute는 해당 객체의 속성이다.</p>
</blockquote>
<h3 id="3-엔티티의-설정을-추가할-수-있다">3. 엔티티의 설정을 추가할 수 있다.</h3>
<p><img src="https://velog.velcdn.com/images/shisu_shin/post/9efbd8fc-f870-4d83-ae2d-ba13152878fe/image.png" alt="">
Codegen 영역에 Class Definition 이라고 되어있는 것을 확인할 수 있다. codegen 옵션은 coredata 모델 파일을 기반으로 관리 객체 클래스를 생성하는 방법을 지정하는 곳이다.</p>
<ul>
<li>class definition 옵션 : 코어데이터가 자동으로 생성된다.</li>
<li>category/Extension : NSManagedObject를 하위 클래스로 만들고, 관리 객체의 속성과 관련된 코드를 분리하여 확장으로 정의할 수 있다.</li>
<li>Manual/None : 관리 객체 클래스를 수동으로 생성해야 함</li>
</ul>
<br>

<h4 id="31-릴레이션을-정의할-수-있다">3.1 릴레이션을 정의할 수 있다</h4>
<p>그 sql에서 외래키 같은 역할인데 쫌 다른거 같다.. 일단 나중에 하겠음.</p>
<br>

<h3 id="4-엔티티가-정의되었다-코어-데이터는-엔티티-구조를-객체-형태로-변한하여-데이터-모델-클래스를-자동으로-생성해-준다-이제-데이터를-저장하고-등록하기-위한-코드-설정을-해주어야-한다">4. 엔티티가 정의되었다. 코어 데이터는 엔티티 구조를 객체 형태로 변한하여 데이터 모델 클래스를 자동으로 생성해 준다! 이제 데이터를 저장하고 등록하기 위한 코드 설정을 해주어야 한다.</h3>
<p>4.1. app delegate 설정 추가
나는 빈 프로젝트에 core data를 추가해주었기 때문에 app delegate에 코어 데이터 스택을 설정하는 코드를 추가해주어야 했다. 
<img src="https://velog.velcdn.com/images/shisu_shin/post/4d073fa0-0f5a-4117-9c15-770d985b17f6/image.png" alt="">
추가한 코드를 간단히만 살펴보자면</p>
<pre><code class="language-swift">var persistentContainer: NSPersistentContainer = {
    let container = NSPersistentContainer(name: &quot;Model&quot;)
    container.loadPersistentStores(completionHandler: { (storeDescription, error) in
        if let error = error as NSError? {
            fatalError(&quot;Unresolved error \(error), \(error.userInfo)&quot;)
        }
    })
    return container
}()</code></pre>
<p>이 구문은 프로젝트에 추가된 xcdatamodelId 파일을 코어 데이터 시스템에 등록하고, 이를 이요하여 NSPersistentContainer 객체를 생성하는 역할을 한다. </p>
<pre><code class="language-swift">func saveContext() {
    let context = persistentContainer.viewContext
    if context.hasChanges {
        do {
            try context.save()
        } catch {
            let nserror = error as NSError
            fatalError(&quot;Unresolved error \(nserror), \(nserror.userInfo)&quot;)
        }
    }
}</code></pre>
<p>saveContext() 메서드의 첫줄을 보면 context 상수에 저장되는 것은 NSManagedObjectContext 객체이다. 코어 데이터에서 데이터를 읽고 쓰기 위해서는 매번 컨텍스트 객체가 필요한데 그때마다 직접생성하는 것이 아니라 persistentContainer 객체의 viewContext 속성을 통해 참조하는 것이였다.!!!🫨🫨🫨🫨🫨</p>
<blockquote>
<p>👩‍🏫 여기서 정리! 우리는 결국 데이터를 패치하거나 저장하거나 수정하려면 model 객체가 아닌 NSManagedObjectContext 객체가 필요한 것이다!! 얘의 역할이 데이터 생성 수정 삭제 등 총관리 였으니까!!! 아웅헷갈려</p>
</blockquote>
<br>

<h3 id="5-이제-데이터를-저장할-수-있다">5. 이제 데이터를 저장할 수 있다.</h3>
<p>그러니까 이제 우린 앱에 데이터를 저장할 수 있고 저장된 데이터를 읽어올 수도 있다. 컨텍스트 객체를 통해서! 그럼 일단 데이터를 저장하는 과정은 어떻게 될까?</p>
<p><span style="background-color:#F6FDC3"><strong>데이터 저장하기</strong></span></p>
<ol start="0">
<li>일단 appdelegate에 있는 NSManagedObjectContext 객체를 가져온다.<pre><code class="language-swift">//앱 델리게이트 객체 참조
let appDelegate = UIApplication.shared.delegate as! AppDelegate
//관리 객체 컨텍스트 참조
let context = appDelegate.persistentContainer.viewContext</code></pre>
</li>
<li>빈 NSManagedObject를 만들고 이를 NSManagedObjectContext 객체에 등록한다.<pre><code class="language-swift">//앱 델리게이트 객체 참조
let appDelegate = UIApplication.shared.delegate as! AppDelegate
//관리 객체 컨텍스트 참조
let context = appDelegate.persistentContainer.viewContext
//빈 관리 객체 생성하고 컨텍스트 객체에 등록
let entity = NSEntityDescription.entity(forEntityName: &quot;Product&quot;, in: context)</code></pre>
</li>
<li>생성된 NSManagedObject에 값을 넣고<pre><code class="language-swift">//앱 델리게이트 객체 참조
let appDelegate = UIApplication.shared.delegate as! AppDelegate
</code></pre>
</li>
</ol>
<p>//관리 객체 컨텍스트 참조
let context = appDelegate.persistentContainer.viewContext</p>
<p>//빈 관리 객체 생성하고 컨텍스트 객체에 등록
let entity = NSEntityDescription.entity(forEntityName: &quot;Product&quot;, in: context)</p>
<p>if let entity = entity {
   let product = NSManagedObject(entity: entity, insertInto: context)
   //값 설정
   product.setValue(&quot;아이폰&quot;, forKey: &quot;title&quot;)
   product.setValue(&quot;123&quot;, forKey: &quot;price&quot;)
}</p>
<pre><code>3. NSManagedObjectContext의 변경사항을 영구 저장소에 반영한다. (이를 커밋이라고 부른다함)
```swift
//앱 델리게이트 객체 참조
let appDelegate = UIApplication.shared.delegate as! AppDelegate

//관리 객체 컨텍스트 참조
let context = appDelegate.persistentContainer.viewContext

//빈 관리 객체 생성하고 컨텍스트 객체에 등록
let entity = NSEntityDescription.entity(forEntityName: &quot;Product&quot;, in: context)

if let entity = entity {
   let product = NSManagedObject(entity: entity, insertInto: context)
   //값 설정
   product.setValue(&quot;아이폰&quot;, forKey: &quot;title&quot;)
   product.setValue(123, forKey: &quot;price&quot;)

   //영구저장소에 커밋
    do {
        try context.save()
    } catch {
        context.rollback()
    }
}</code></pre><p>이렇게 저장한 데이터가 제대로 저장되어 있는지 패치도 해야 한다...</p>
<h3 id="51-데이터-패치도-가능하겠지요">5.1. 데이터 패치도 가능하겠지요</h3>
<p><span style="background-color:#F6FDC3"><strong>데이터 패치하기</strong></span></p>
<p>코어 데이터에 저장된 데이터를 가져올 때에는 요청 객체 NSFetchRequest 객체를 사용한다.! 또 새로운놈이 등장해버림.. 이 객체는 다양한 요청들을 복합적으로 정의할 수 있다. 
그래서 데이터를 패치하는 방법은 저장하는 방법과 비슷하면서도 약간 다른데, 일단 데이터를 조회 하는 것이기에 NSManagedObjectContext 객체를 참조할 것이고, 요청 객체를 생성하여 데이터를 가져올 것이다.</p>
<ol>
<li>일단 appdelegate에 있는 NSManagedObjectContext 객체를 가져온다.</li>
<li>요청 객체를 생성</li>
<li>데이터 가져오기</li>
</ol>
<pre><code class="language-swift">do {
    let fetchRequest: NSFetchRequest&lt;Product&gt; = Product.fetchRequest()
    let products = try context.fetch(fetchRequest)
        products.forEach {
            print($0.title ?? &quot;No Title&quot;, $0.price)
        }
   } catch {
           print(&quot;Error fetching data: \(error)&quot;)
    }</code></pre>
<p>실행하면 콘솔창에 다음과 같이 나온걸 확인할 수 있다.
<img src="https://velog.velcdn.com/images/shisu_shin/post/3ee6a68e-5af4-4cb9-b5a7-f36daeed3c06/image.png" alt=""></p>
<p>설명은 난중에 할게요...🫠 나 과제 언제 시작하뮤ㅠㅠ</p>
<br>

<hr>
<p>참고
<a href="https://developer.apple.com/documentation/coredata/creating_a_core_data_model">1</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[문풀] string to ascii, ascii to string]]></title>
            <link>https://velog.io/@shisu_shin/%EB%AC%B8%ED%92%80-string-to-ascii-ascii-to-string</link>
            <guid>https://velog.io/@shisu_shin/%EB%AC%B8%ED%92%80-string-to-ascii-ascii-to-string</guid>
            <pubDate>Wed, 17 Apr 2024 03:57:13 GMT</pubDate>
            <description><![CDATA[<h3 id="⚙️-시저암호">⚙️ 시저암호</h3>
<p><img src="https://velog.velcdn.com/images/shisu_shin/post/f2b707fe-6071-4a2a-b774-59a5f409ea98/image.png" alt="">
일단 이 문제를 보자마자 아스키코드를 사용해야겠다! 라는 생각까지는 했다. 그러나 문제는 내가 swift에서 문자를 아스키코드로 변환하는 방법을 모른다는 것이다.🫥 그래서 검색을 해보았다. 이 문제에서는 문자열을 아스키코드로 변환하면 아주 편한데 문자열을 아스키코드로 변환하는 방법은 없고 문자(char)를 아스키코드로 변환하는 방법을 찾을 수 있었다.</p>
<pre><code class="language-swift">let char: Character = &quot;A&quot;
let scalarValue = Unicode.Scalar(String(char))!

let asciiCode = scalarValue.value
print(asciiCode)</code></pre>
<p>위의 코드는 문자를 아스키코드로 변환하는 방법인데 먼저 char를 scalarValue로 바꾼후 scalarValue를 asciiCode로 변환하는 원리이다. 반대로 아스키코드를 문자로 복원하는 방법은 다음과 같다. </p>
<pre><code class="language-swift">let asciiCode: UInt8 = 65 
let scalarValue = Unicode.Scalar(asciiCode)

let character = scalarValue.description
print(character)</code></pre>
<p>이외에도 이 문제에서 생각해야하는 것은 단순히 ascii코드를 옆으로 밀어주는 것 뿐만 아니라 밀어준 아스키코드값이 문자 범위를 벗어나면 다시 문자열로 복원해주어야 하며, 공백 문자는 밀어주지 말아야 한다.</p>
<p>그렇게 제출한 답은 다음과 같다.</p>
<pre><code class="language-swift">func solution(_ s:String, _ n:Int) -&gt; String {
    var charToAscii: [UInt8] = []
    var asciiToChar = &quot;&quot;
    for char in s {
        if 65 &lt;= char.asciiValue! &amp;&amp; char.asciiValue! &lt;= 90 {
            if char.asciiValue! + UInt8(n) &gt; 90 {
                charToAscii.append(char.asciiValue! + UInt8(n) - 26)
            } else {
                charToAscii.append(char.asciiValue! + UInt8(n))
            }
        } else if char == &quot; &quot; {
            charToAscii.append(char.asciiValue!)
        } else{
            if char.asciiValue! + UInt8(n) &gt; 122 {
                charToAscii.append(char.asciiValue! + UInt8(n) - 26)
            } else {
                charToAscii.append(char.asciiValue! + UInt8(n))
            }
        }
    }
    //print(charToAscii)
    for asc in charToAscii {

        asciiToChar.append(String(UnicodeScalar(asc)))
    }
    //print(asciiToChar)
    return asciiToChar
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[iOS] 깃허브 프로필 조회 화면 만들기 [2]]]></title>
            <link>https://velog.io/@shisu_shin/iOS-%EA%B9%83%ED%97%88%EB%B8%8C-%ED%94%84%EB%A1%9C%ED%95%84-%EC%A1%B0%ED%9A%8C-%ED%99%94%EB%A9%B4-%EB%A7%8C%EB%93%A4%EA%B8%B0-2</link>
            <guid>https://velog.io/@shisu_shin/iOS-%EA%B9%83%ED%97%88%EB%B8%8C-%ED%94%84%EB%A1%9C%ED%95%84-%EC%A1%B0%ED%9A%8C-%ED%99%94%EB%A9%B4-%EB%A7%8C%EB%93%A4%EA%B8%B0-2</guid>
            <pubDate>Tue, 16 Apr 2024 11:25:55 GMT</pubDate>
            <description><![CDATA[<h2 id="📝-kingfisher로-이미지-불러오기">📝 KingFisher로 이미지 불러오기</h2>
<p>서버에서 프로필 이미지는 url로 넘겨주며 이 url은 String 타입으로 준다. 이 url은 웹서버에서 이미지의 경로다! 그래서 우리는 이 url로 접속해서 이미지를 로드해올 수 있다.</p>
<pre><code class="language-swift">guard let url = URL(string: profile.avatar_url) else {return}
self.imageView.kf.setImage(with: url)</code></pre>
<p>위 코드의 profile타입에 저장된 avatar_url은 서버에서 받아온 프로필 사진 주소이다. 이를 URL 형식으로 변환해준후 kingFisher를 이용해 imageView에 image를 로드해주었다. 사용하는 방법은 은근 간단한듯... 근데 <a href="https://github.com/onevcat/Kingfisher">깃</a> 보니까 제공하는 기능이 짱 많다..!</p>
<blockquote>
<p>ios에도 기본적으로 제공되는 이미지 로드 기능이 있다! 하지만 kingFisher를 사용하는 것이 좋다! 왜? *<em>이미지 다운로드와 캐싱기능을 자동으로 해준다. 그래서 메모리 관리가 편하다! *</em> 아직 완벽하게 이해는 안되지만 대충 뭔소린지 알겠음..</p>
</blockquote>
<p><br><br></p>
<h2 id="📝-pull-to-refresh-기능-구현">📝 Pull to refresh 기능 구현</h2>
<p>pull to refresh는 tableView나 collectionView에서 위로 댕겼을때 데이터를 새로 받아오는 기능을 말한다. 말 그대로 pull 당겨서 refresh 새로고침! 이 기능은 처음 구현해보아서 구글에서 많은 도움을 받아 구현하였다. </p>
<pre><code class="language-swift">repoTableView.refreshControl = refreshControll
refreshControll.addTarget(self, action: #selector(refreshFunction), for: .valueChanged)

@objc func refreshFunction(){
    repoTableView.reloadData()
}</code></pre>
<h2 id="📝-paging-기능-구현">📝 Paging 기능 구현</h2>
<p>페이징 기능은 테이블뷰 위의 화살표로 다음 페이지로 넘어갈 수 있도록 구현했다. 이때 조금 애먹었던 사항이 만약 레포가 14개 있다면 맨 마지막 페이지에는 테이블뷰가 4개만 나오도록 구현하도록, 그러니까 빈 테이블 6개와 4개가 아닌 그냥 테이블셀만 4개 나오도록 하는 것이였다.
그렇게 하기 위해 테이블 셀의 갯수를 지정하는 메서드를 다음과 같이 구현하였다.
<img src="https://velog.velcdn.com/images/shisu_shin/post/c677e55e-ebad-4899-ad31-106d978a1370/image.png" alt="">
그리고 화살표를 눌렀을때의 메서드는 다음과 같이 구현하였다.
<img src="https://velog.velcdn.com/images/shisu_shin/post/37efec16-141e-452c-81d6-4bd393684f3c/image.png" alt=""></p>
<p>그렇게 구현한 최종화면은 다음과 같다. 내 레포가 14개밖에 되지 않아서 딱히 페이징 기능을 해야했나 생각을잠시 했지만 일단은 만족! 그런데 refreshControl이 제대로 작동하지 않는것 같다...
<img src="https://velog.velcdn.com/images/shisu_shin/post/0d3d4533-25f4-420c-8579-3208da0e8848/image.gif" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[문풀] for 루프의 반복변수는 무조건 상수취급]]></title>
            <link>https://velog.io/@shisu_shin/%EB%AC%B8%ED%92%80-for-%EB%A3%A8%ED%94%84%EC%9D%98-%EB%B0%98%EB%B3%B5%EB%B3%80%EC%88%98%EB%8A%94-%EB%AC%B4%EC%A1%B0%EA%B1%B4-%EC%83%81%EC%88%98%EC%B7%A8%EA%B8%89</link>
            <guid>https://velog.io/@shisu_shin/%EB%AC%B8%ED%92%80-for-%EB%A3%A8%ED%94%84%EC%9D%98-%EB%B0%98%EB%B3%B5%EB%B3%80%EC%88%98%EB%8A%94-%EB%AC%B4%EC%A1%B0%EA%B1%B4-%EC%83%81%EC%88%98%EC%B7%A8%EA%B8%89</guid>
            <pubDate>Tue, 16 Apr 2024 07:33:54 GMT</pubDate>
            <description><![CDATA[<p>최근에 알고리즘 문제는 다소 무난하게 풀리고 있었는데.. 오늘 이 문제 때문에 시간을 많이 써버렸다 허허 
<br></p>
<h3 id="⚙️-최소직사각형">⚙️ 최소직사각형</h3>
<p><img src="https://velog.velcdn.com/images/shisu_shin/post/5809b1bf-df4c-494d-94ad-d02bfb8601dc/image.png" alt="">
문제는 다음과 같고 입출력 예시는 다음과 같다. <img src="https://velog.velcdn.com/images/shisu_shin/post/8a9daa67-f2e3-4030-ab63-dd298903fac2/image.png" alt=""></p>
<p>어찌저찌 머리를 굴려 일단 각 직사각형마다, 즉 이차원배열의 요소들마다 가로길이와 세로길이를 비교해 큰값을 무조건 가로길이로 위치를 바꾸고 모든 직사각형의 가로길이중 최대길이와 세로길이중 최대길이를 곱하는 방식으로 문제를 풀려고 하였다.</p>
<p>그래서 처음으로 작성한 코드는 다음과 같다.
<img src="https://velog.velcdn.com/images/shisu_shin/post/917be0a9-5af9-47ad-b179-f6835e963390/image.png" alt="">
이차원 배열을 분명 var input으로 변수로 선언했는데도 for 문안에서 위치를 바꾸려니 다음과 같은 에러가 났다. Cannot assign to property: &#39;size&#39; is a &#39;let&#39; constant&quot; 이 에러가 발생하는 이유는 <strong>for 루프의 반복 변수는 상수(let)로 취급되기 때문에 수정할 수 없어서이다!!</strong> 그래서 다음과 같이 수정하니 정상작동되었다.
<img src="https://velog.velcdn.com/images/shisu_shin/post/575b5f8b-82d0-4774-81fa-87e49d185ff6/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[iOS] 깃허브 프로필 조회 화면 만들기 [1]]]></title>
            <link>https://velog.io/@shisu_shin/iOS-%EA%B9%83%ED%97%88%EB%B8%8C-%ED%94%84%EB%A1%9C%ED%95%84-%EC%A1%B0%ED%9A%8C-%ED%99%94%EB%A9%B4-%EB%A7%8C%EB%93%A4%EA%B8%B0-1</link>
            <guid>https://velog.io/@shisu_shin/iOS-%EA%B9%83%ED%97%88%EB%B8%8C-%ED%94%84%EB%A1%9C%ED%95%84-%EC%A1%B0%ED%9A%8C-%ED%99%94%EB%A9%B4-%EB%A7%8C%EB%93%A4%EA%B8%B0-1</guid>
            <pubDate>Mon, 15 Apr 2024 09:54:12 GMT</pubDate>
            <description><![CDATA[<p>과제의 조건은 다음과 같다.
<img src="https://velog.velcdn.com/images/shisu_shin/post/5fcd9b4a-69a5-4f50-b8d5-41ce36d2f267/image.png" alt=""></p>
<ul>
<li>alamofire와 kingFisher를 사용할 것</li>
<li>pull to refresh 기능 구현할 것</li>
<li>페이징 처리하게 할것</li>
<li>tableView나 collectionView 사용한것</li>
</ul>
<br>
<br>

<h2 id="📝-깃-api-어떻게-받음">📝 깃 api 어떻게 받음??</h2>
<p>일단 깃허브에 api가 있다는 사실을 처음알았다!!!! 이렇게 유용한 api가 있었다니..!!😮😮😮 
깃허브에서 제공하는 api 정보를 보고 api 키를 받았다. 깃허브에서 response로 응답하는 정보가 정말 많았다.
일단 나는 </p>
<ol>
<li>사용자 <a href="https://docs.github.com/ko/rest/users/users?apiVersion=2022-11-28">프로필 정보</a></li>
<li>사용자의 <a href="https://docs.github.com/ko/rest/repos/repos?apiVersion=2022-11-28">레포 정보</a>
를 받아 사용할 것이다.</li>
</ol>
<p>더 직관적으로 알아보기 위해 postman 에서 테스트 해보았다. 먼저 내 깃허브를 조회한 결과 다음과 같은 다양한 정보를 받았다. 
<img src="https://velog.velcdn.com/images/shisu_shin/post/5568b83e-dbdb-44b2-9fcd-637b517a8921/image.png" alt="">
이중에서 bio와 팔로잉, 팔로워, 레포 수와 같은 정보를 사용하기로 했다. 그리고 repo에 대한 api를 테스트 해본 결과
<img src="https://velog.velcdn.com/images/shisu_shin/post/af10fed3-5535-4aa6-a1cd-c6e6858f8278/image.png" alt="">
를 응답받았다.
<br>
<br></p>
<h2 id="📝-디자인-구상하기">📝 디자인 구상하기</h2>
<p>api를 통해 사용할 정보를 정해 생각한 디자인은 다음과 같다. 인스타를 참고했다. (머릿속으로 구상한거여서 일단 완성본을 첨부한다!ㅎㅎ😎)
<img src="https://velog.velcdn.com/images/shisu_shin/post/d1a53c9e-f4ef-4b71-bd2b-2090cc491040/image.png" width="40%" height="40%">
아니 벨로그에서 사진 크기 어떻게 줄임..--
<br>
<br></p>
<h2 id="📝-tableview-사용하기">📝 tableView 사용하기</h2>
<p>작업의 속도를 높이기 위해 storyboard를 사용해 구현했다. tableViewCell는 한 화면에 10개씩 나오게 하였으며 페이징처리로 다음 레포를 볼 수 있도록 구현할 예정이였다. 스토리보드를 사용했기때문에 cell을 스토리보드내에서 셀 등록 해주고 사용했다.
<br>
<br></p>
<h2 id="📝-alamofire-사용하기">📝 alamofire 사용하기</h2>
<p>alamofire는 전에 사용해본 경험이 있었다! 그래서 그때의 코드를 참고했다....(과거의 나자신 기특혀...) 나는 spm을 이용하여 alamofire를 import하였다. 일단 응답받는 구조체를 만들고 callApi 함수로 api를 받았다. </p>
<pre><code class="language-swift">//응답받는 구조체
struct Profile: Codable {
    let login: String
    let followers: Int
    let following: Int
    let public_repos: Int
    let bio: String
    let avatar_url: String
    let repos_url: String
}

//서버에 리퀘보내고 응답받아오는 함수
func callProfileAPI(completion: @escaping (Profile?) -&gt; Void) {
        let url = &quot;https://api.github.com/users/jiyeondu&quot;
        let param = [&quot;auth&quot; : &quot;&quot;]
        AF.request(url, method: .get, parameters: param).responseDecodable(of: Profile.self){
            (response) in
            switch response.result {
            case .success(let value):
                print(value)
                completion(value)
            case .failure(let error):
                print(&quot;ProfileError: \(error)&quot;)
            }
        }
    }</code></pre>
<p>(인증키는 비밀!)
위의 함수를 설명하자면 일단 </p>
<ol>
<li><code>AF.request(url, method: .get, parameters: param)</code> 여기서 alamofire를 이용해서 url에 get 요청을 보낸다. 그리고 <code>.responseDecodable(of: Profile.self)</code>를 통해 받은 응답을 <code>Profile</code> 타입으로 디코딩 한다. url에서는 응답을 json 형태로 보내준다.</li>
<li>만약 response.result가 성공적이면 <code>value</code> 매개변수에 디코딩된 <code>Profile</code> 객체가 전달되며, 이를 completion 핸들러를 통해 완료</li>
<li><code>completion</code> 매개변수는 클로저 형태로 전달되며, Profile? 타입의 옵셔널 값을 받는다. 이 핸들러는 프로필 정보를 성공적으로 가져온 후에 호출된다(completion 핸들러 특징!)</li>
</ol>
<blockquote>
<p><a href="https://velog.io/@wannabe_eung/Swift-CompeltionHandler%EB%A5%BC-%EC%98%88%EC%8B%9C%EB%A5%BC-%ED%86%B5%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90">completion handler를 모른다면? </a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[iOS] subway 주문 화면 만들기 [2]]]></title>
            <link>https://velog.io/@shisu_shin/iOS-subway-%EC%A3%BC%EB%AC%B8-%ED%99%94%EB%A9%B4-%EB%A7%8C%EB%93%A4%EA%B8%B0-2</link>
            <guid>https://velog.io/@shisu_shin/iOS-subway-%EC%A3%BC%EB%AC%B8-%ED%99%94%EB%A9%B4-%EB%A7%8C%EB%93%A4%EA%B8%B0-2</guid>
            <pubDate>Tue, 09 Apr 2024 12:15:30 GMT</pubDate>
            <description><![CDATA[<p>스토리보드 하나를 가지고 협업하는 것은 무리였다... 그래서 우리 팀은 스토리보드 없이 코드로 UI를 구현하기로 결심했다. 그리고 UI 디자인 가이드를 피그마로 만들었다. 디자인가이드는 다음과 같다. 
<img src="https://velog.velcdn.com/images/shisu_shin/post/c9ac225f-198d-4c5b-bddf-949ae4630348/image.png" alt="">
여기서 내가 맡은 부분은 상단부이고, 크기는 고정되어 있다. 내가 구현한 부분은 스토리보드로 구현했을 때와 코드가 거의 유사하고 특별히 추가되거나 하는 것은 없었지만 다른 팀원분의 코드와 결합하는 과정에서 고민할 부분이 많았다. </p>
<ul>
<li>코드로 autolayout 구현시 유의할점</li>
<li>하나의 뷰에 여러 콜렉션뷰 넣기</li>
</ul>
<h3 id="✅-코드로-autolayout-구현시-유의할-점">✅ 코드로 autolayout 구현시 유의할 점</h3>
<p><code>.translatesAutoresizingMaskIntoConstraints = false</code>로 설정해야한다. 코드를 통해 뷰를 생성할 때는 이 속성이 기본적으로 true로 설정된다. 뷰가 자동으로 해당 부모의 뷰의 크기 및 위치에 맞게 조정되도록 해주는 것인데 이 속성이 true로 설정되어 있으면 autolayout의 제약조건이 적용되지 않는다. 그래서 이 속성을 꼭 false로 해줘야한다. 몇몇 뷰에 이 속성을 설정하지 않아 애를 많이 먹었다.</p>
<h3 id="✅-하나의-뷰에-여러-콜렉션뷰-넣기">✅ 하나의 뷰에 여러 콜렉션뷰 넣기</h3>
<p>subway화면의 중단부를 보면 여러개의 가로스크롤이 되는 콜렉션뷰가 사용된다. 가로스크롤을 위해 collectionViewFlowlayout을 설정해준다. </p>
<pre><code class="language-swift">let topCollectionViewFlowLayout: UICollectionViewFlowLayout = {
        let flowlayout = UICollectionViewFlowLayout()
        flowlayout.scrollDirection = .horizontal
        flowlayout.minimumLineSpacing = 15
        flowlayout.sectionInset = UIEdgeInsets(top:0, left:0, bottom: 0, right: 0)
        return flowlayout
    }()

lazy var tabbarTop: UICollectionView = {
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: topCollectionViewFlowLayout)
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        return collectionView
    }()</code></pre>
<p>이게 내가 맡은 부분인 topView에 해당하는 부분이라면 팀원분의 코드를 합치는 과정에서 문제가 생겼다. 추가된 팀원 코드는 다음과 같았다.</p>
<pre><code class="language-swift">// middle view
    let collectionViewFlowLayout: UICollectionViewFlowLayout = {
        let flowlayout = UICollectionViewFlowLayout()
        flowlayout.scrollDirection = .horizontal
        flowlayout.minimumLineSpacing = 15
        flowlayout.sectionInset = UIEdgeInsets(top:0, left:0, bottom: 0, right: 0)
        return flowlayout
    }()

    lazy var breadHorizontalBar: UICollectionView = {
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewFlowLayout)
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        return collectionView
    }()</code></pre>
<p>여기서 확인해보면 collectionViewFlowLayout이 중복되어있는것을 확인할 수 있다. 그래서 중복된 코드를 줄이고자 하나의 collectionViewFlowLayout을 사용해서 모든 콜렉션뷰가 채택할 수 있도록 하게 했다. </p>
<pre><code class="language-swift">    let collectionViewFlowLayout: UICollectionViewFlowLayout = {
        let flowlayout = UICollectionViewFlowLayout()
        flowlayout.scrollDirection = .horizontal
        flowlayout.minimumLineSpacing = 15
        flowlayout.sectionInset = UIEdgeInsets(top:0, left:0, bottom: 0, right: 0)
        return flowlayout
    }()

    lazy var breadHorizontalBar: UICollectionView = {
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewFlowLayout)
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        return collectionView
    }()

    lazy var tabbarTop: UICollectionView = {
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewFlowLayout)
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        return collectionView
    }()</code></pre>
<p>같은 flowLayout을 채택한 후 dataSource와 delegate 프로토콜의 메서드를 구현할 때도 다음과 같이 했다.</p>
<pre><code class="language-swift">func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -&gt; Int {
        if collectionView == tabbarTop {
            return menuList.count
        } else if collectionView == breadHorizontalBar {
            return breadList.count
        } ...

    }</code></pre>
<p>if문으로 받는 collectionView를 구분해서 무사히 구현할 수 있었지만 문제가 발생했다. <code>func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -&gt; CGSize</code> 메서드르르 구현할 때 위와 같이 if문으로 collectionView만을 분리하니까 분리가 되지 않았다.</p>
<pre><code class="language-swift">
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -&gt; CGSize {
        if collectionView == tabbarTop {
            let text = menuList[indexPath.row]
            let font = UIFont.systemFont(ofSize: 20)
            let textWidth = text.size(withAttributes: [NSAttributedString.Key.font: font]).width
            let cellWidth = textWidth + 20
            return CGSize(width: cellWidth, height: 60)
        } else {
            let text = breadList[indexPath.row]
            let font = UIFont.systemFont(ofSize: 18)
            let textWidth = text.size(withAttributes: [NSAttributedString.Key.font: font]).width
            let cellWidth = textWidth + 10
            return CGSize(width: cellWidth, height: 20)
        }</code></pre>
<p>위의 코드를 실행하면 if 문을 인식하지 못하고 무조건 else 문으로 빠져버린다. 글래서 tabbarTop이 다음과 같이 나온다.ㅠㅠ
<img src="https://velog.velcdn.com/images/shisu_shin/post/35de4a74-d254-46f6-8257-fac0c6930787/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[문풀] string의 index는 단순 int타입으로 접근x]]></title>
            <link>https://velog.io/@shisu_shin/%EB%AC%B8%ED%92%80-string%EC%9D%98-index%EB%8A%94-%EB%8B%A8%EC%88%9C-int%ED%83%80%EC%9E%85%EC%9C%BC%EB%A1%9C-%EC%A0%91%EA%B7%BCx</link>
            <guid>https://velog.io/@shisu_shin/%EB%AC%B8%ED%92%80-string%EC%9D%98-index%EB%8A%94-%EB%8B%A8%EC%88%9C-int%ED%83%80%EC%9E%85%EC%9C%BC%EB%A1%9C-%EC%A0%91%EA%B7%BCx</guid>
            <pubDate>Fri, 05 Apr 2024 06:12:34 GMT</pubDate>
            <description><![CDATA[<h3 id="⚙️-이상한-문자-만들기">⚙️ 이상한 문자 만들기</h3>
<p><img src="https://velog.velcdn.com/images/shisu_shin/post/673259b1-9c0a-4869-b758-5b6e1f5729c1/image.png" alt="">
아놔 스트링 index접근을 매일 까먹는다..바보냐고!!!!! 제목에서 나의 빡침이 느껴질련지는 모르겠지만 이참에 확실히 외워야 겠다는 생각을 다시 한다..</p>
<hr>
<h4 id="stringindex_offsetby"><code>String.index(_:offsetBy:)</code></h4>
<p>문자열에서 특정 인텍스를 찾기위한 메서드이다. 첫번째 매개변수에는 인덱스를 계산하는 기준이 되는 위치(startIndex)를, 두번째 매개변수에는 위치에서부터의 이동ㄱ리를 나타내는 정수값을 넣는다.</p>
<pre><code class="language-swift">let str = &quot;Hello, World!&quot;

// 문자열의 시작 위치부터 7번째 문자까지의 인덱스를 찾음
let index = str.index(str.startIndex, offsetBy: 7)

// 찾은 인덱스를 사용하여 해당 위치의 문자에 접근
print(str[index]) // 출력: &quot;W&quot;</code></pre>
<hr>
<p><img src="https://velog.velcdn.com/images/shisu_shin/post/eb01d001-25cf-4975-9f23-d09877ba560f/image.png" alt="">
<code>Subscript &#39;subscript(_:)&#39; requires that &#39;String.Index&#39; conform to &#39;RangeExpression&#39;</code> : 문자열을 인덱싱할 때 발생하는 에러라고 한다. 문자열을 인덱싱하기 위해서는 w즉 []를 통해 접근하기 위해서는 이 괄호 사이에 String.index타입이 들어가야하는데 그 타입이 들어가지 않았다는것!! 아니 그런데 바로 위의 if절에는 에러가 안나는데 이 코드에서 에러가 나는 이유를 모르겠다🤬🤬🤬 </p>
<p>혹시 몰라서 index 부분을 분리해서 코드를 짜보았다. 그러니까 에러가 사라졌다... 황당하다
<img src="blob:https://velog.io/c313282d-0fcc-441c-ab5f-e861ffe73509" alt="업로드중.."></p>
<p>그리고 리스트를 string으로 만드는 방법으로 .joined()메서드를 사용했다. </p>
<hr>
<h4 id="joined"><code>.joined()</code></h4>
<p>이 메서드는 배열에 사용되는 메서드로, 배열의 각 요소를 하나의 문자열로 결합하는데 사용된다고 한다. 배열에 포함된 요소들을 모두 이어붙여 두는데 요소들 사이에 구분자를 추가할 수도 있다. 요소들의 구분자를 추가하려면, 예를들어 요소들사이에 한칸씩 뛰우려면 .joined(seperator: &quot; &quot;)를 사용하면 된다.</p>
<pre><code class="language-swift">let fruits = [&quot;Apple&quot;, &quot;Banana&quot;, &quot;Orange&quot;]
let result = fruits.joined()
print(result) // 출력: &quot;AppleBananaOrange&quot;</code></pre>
<hr>
<p>그렇게 제출한 정답 코드는 다음과 같다.</p>
<pre><code class="language-swift">func solution(_ s:String) -&gt; String {
    var arr: [String] = []
    var x = 0
    for i in s {
        if x % 2 == 0 {
            arr.append(String(i.uppercased()))
        } else {
            arr.append(String(i.lowercased()))
        }
        x += 1
        if i == &quot; &quot; {
            x = 0
        }
    }
    return arr.joined()
}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[깃허브에 화살표 폴더]]></title>
            <link>https://velog.io/@shisu_shin/%EA%B9%83%ED%97%88%EB%B8%8C%EC%97%90-%ED%99%94%EC%82%B4%ED%91%9C-%ED%8F%B4%EB%8D%94</link>
            <guid>https://velog.io/@shisu_shin/%EA%B9%83%ED%97%88%EB%B8%8C%EC%97%90-%ED%99%94%EC%82%B4%ED%91%9C-%ED%8F%B4%EB%8D%94</guid>
            <pubDate>Thu, 04 Apr 2024 06:48:23 GMT</pubDate>
            <description><![CDATA[<h3 id="📖-문제">📖 문제</h3>
<p><img src="https://velog.velcdn.com/images/shisu_shin/post/2cc63ed6-fc5e-4836-835b-9f5f8298d119/image.png" alt=""></p>
<p>이렇게 깃허브의 폴더에 화살표가 떠있고 들어가지지 않는 경우가 있다.<span style="color:DarkOrange"> *<em>이런 문제가 발생하는 경우 subway 폴더 안에 .git 파일이 있어서 이다!! *</em></span> 실제로 저 폴더는 다른 깃 레포가 연결되어 있는 폴더의 하위에서 가져왔다. 
커맨드창으로 실제 로컬의 파일들을 확인해보니
<img src="https://velog.velcdn.com/images/shisu_shin/post/b25c7f11-0d55-4ccd-9d9f-77302531adce/image.png" alt="">
.git 파일이 있음을 확인할 수 있다.</p>
<p>문제를 해결하는 방법은 .git 일을 제거하고, 스테이지에 이미 올라간 파일을 제거하는 것이다.</p>
<h3 id="📖-해결방법">📖 해결방법</h3>
<p><span style="background-color:#FFF6B9">1. .git 파일 제거</p>
<pre><code class="language-git">rm -rf .git</code></pre>
<p><img src="https://velog.velcdn.com/images/shisu_shin/post/9b1cea85-8f4e-4aa0-9665-5fedbe274380/image.png" alt=""></p>
<p><span style="background-color:#FFF6B9">2. 스테이지 파일 제거</p>
<pre><code class="language-git">git rm --cached . -rf</code></pre>
<p><img src="https://velog.velcdn.com/images/shisu_shin/post/814d5013-af7f-48a0-9273-69d494c32a05/image.png" alt="">
<span style="background-color:#FFF6B9">3. 다시 add, commit, push 진행</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[iOS] subway 주문 화면 만들기 [1]]]></title>
            <link>https://velog.io/@shisu_shin/iOS-subway-%EC%A3%BC%EB%AC%B8-%ED%99%94%EB%A9%B4-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@shisu_shin/iOS-subway-%EC%A3%BC%EB%AC%B8-%ED%99%94%EB%A9%B4-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Thu, 04 Apr 2024 06:33:20 GMT</pubDate>
            <description><![CDATA[<p>미니 프로젝트로 팀원분들과 subway 주문 화면을 만들어 보기로 했다!
<img src="https://velog.velcdn.com/images/shisu_shin/post/30d2c2ad-7e01-4293-8ac4-ea9ff4944558/image.png" alt=""></p>
<p>구상한 화면은 다음과 같다. 여기서 top, body, bottom으로 3부분으로 나누어 작업하기로 했고 나는 top 부분을 맡았다. 스토리보드로 구현하기로 했으면 협업은 깃을 활용했다. 사실 하나의 뷰컨트롤러를 여러명이 동시에 작업한다는 점이 충돌을 피하기 어렵겠지만 일단 도전해보기로 했다.</p>
<h2 id="🧑🏫-topview-만들기">🧑‍🏫 TopView 만들기</h2>
<p>먼저 topView 에서 titleView와 menuView로 구분하고 만들었다.
titleView는 UIView위에 UILabel을 올리기만 하면 된다.</p>
<p>콜렉션뷰를 사용해서 상단 탭바를 만들어보았다. 콜렉션뷰는 테이블 뷰와 마찬가지로 dataSource와 delegate를 지정해주어야 했고 셀을 등록시켜줘야 했다. 콜렉션 뷰를 만드는 것은 어렵지 않았으나 다음과 같은 사항을 지키는 것이 중요했다.</p>
<ul>
<li>가로 스크롤일 것</li>
<li>콜렉션뷰의 셀이 메뉴의 글자 길이에 맞는 크기를 가질 수 있도록 할 것</li>
<li>클릭 시에 글자의 색이 바뀌거나 배경 색이 바뀌도록 할것</li>
</ul>
<h3 id="✅-collectionviewflowlayout">✅ collectionViewFlowLayout</h3>
<p>먼저 가로스크롤을 구현하기 위해 collectionViewFlowLayout()이라는 클래스를 사용하였다.
이 클래스는 컬렉션 뷰의 레이아웃을 정의하고 사용자가 정의한 규칙에 따라 셀을 배치한다. 여기서 말하는 레이아웃은 화면에 표시되는 요소들의 위치, 크기, 간격 등을 말한다고 생각하면 된다.
<img src="https://velog.velcdn.com/images/shisu_shin/post/549fd0dd-b010-4fc0-b959-1ec99eae3b36/image.png" alt=""></p>
<p>공식 문서에 나와있는 내용을 보면 이 클래스의 <code>scrollDirection</code> 프로퍼티를 활용하여 스크롤 방향을 수직이나 수평으로 바꿔줄 수 있다. </p>
<pre><code class="language-swift">func setTabbarTop(){
        let flowlayout = UICollectionViewFlowLayout()
        flowlayout.scrollDirection = .horizontal        //가로스크롤!!
        flowlayout.minimumLineSpacing = 15
        flowlayout.sectionInset = UIEdgeInsets(top:0, left:0, bottom: 0, right: 0)
    }</code></pre>
<p>뿐만 아니라 <code>.minimumLineSpacing</code> 으로 아이템간의 간격을 정할 수 있었고, <code>.sectionInset</code> 으로 셀과 섹션의 가장자리 간의 간격을 조절할 수 있다. </p>
<p>collectionView는 collectionFlowLayout을 다음과 같이 지정해주었다ㅏ.</p>
<pre><code class="language-swift">let topCollectionViewFlowLayout: UICollectionViewFlowLayout = {
        let flowlayout = UICollectionViewFlowLayout()
        flowlayout.scrollDirection = .horizontal
        flowlayout.minimumLineSpacing = 15
        flowlayout.sectionInset = UIEdgeInsets(top:0, left:0, bottom: 0, right: 0)
        return flowlayout
    }()</code></pre>
<p>scrollDirection은 .horizontal로 가로로 스크롤되도록 설정하였고, .minumumLineSpacing으로 아이템의 세로간격을 15포인트로 설정하였다. 그리고 .sectionInset으로 모든 여백을 0으로 설정하여 섹션 내부의 여백을 제거하였다.
그리고 해당 collectionView가 topCollectionViewFlowLayout을 따를 수 있도록 설정해주었다. 여기서 frame: .zero는 초기 프레임이 없고 추후에 크기를 설정해주어야 한다는 뜻이다. (나는 오토레이아웃으로 추후에 크기와 위치를 설정하였다.)</p>
<pre><code class="language-swift">lazy var tabbarTop: UICollectionView = {
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: topCollectionViewFlowLayout)
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        return collectionView
    }()</code></pre>
<h3 id="✅-textwidth">✅ textWidth</h3>
<p> 콜렉션뷰의 셀이 메뉴의 글자 길이에 맞는 크기를 가질 수 있도록 하기 위해서는 텍스트의 길이를 알아야한다. 이때 맨처음에는 깊은 생각없이 text.count를 했더니 텍스트의 문자길이가 나왔다.🥲 바보같은 실수였다..ㅎ 그래서 textWidth를 구하기 위해 찾아보니 text의 사이즈를 구할 수 있는 코드를 찾았다.</p>
<pre><code class="language-swift">let text = menuList[indexPath.row]
let textWidth = text.size(withAttributes: [NSAttributedString.Key.font: font]).width</code></pre>
<p>text의 size(withAttributes:)메서드는 NSAttributedString의 확장 메서드로, 문자열이 주어진 속성과 폰트로 렌더링될 때의 크기를 반환한다. 속성으로 전달되는 딕셔너리인 <code>[NSAttributedString.Key.font: font]</code>는 문자열의 폰트를 설정하는 키와 문자열에 적용할 폰트로 구성되어 있다. 
그렇게 셀을 textWidth보다 20정도 크게 하는 코드는 다음과 같다.</p>
<pre><code class="language-swift">let text = menuList[indexPath.row]
let font = UIFont.systemFont(ofSize: 20)
let textWidth = text.size(withAttributes: [NSAttributedString.Key.font: font]).width
let cellWidth = textWidth + 20
return CGSize(width: cellWidth, height: 60)</code></pre>
<h3 id="✅-isselected">✅ isSelected</h3>
<p>클릭 시에 글자의 색이 바뀌거나 배경 색이 바뀌도록 해야했다. didSelectItemAt 메서드를 사용해서 구현하려고 했지만 검색결과 cell 클래스에서 해결할 수 있는 방법이 있었다. 
isSelected 속성을 오버라이드하여 해당 뷰가 선택되었을 때와 선택되지 않았을 대의 UI를 변경할 수 있다. 해당 뷰의 isSelected에 프로퍼티 감시자를 정의하여 다음과 같이 구현하였다. </p>
<pre><code class="language-swift">override var isSelected: Bool {
        didSet{
            if isSelected {
                backView.backgroundColor = UIColor.lightGray
                menuText.textColor = .black
            }
            else {
                menuText.textColor = .gray
                backView.backgroundColor = UIColor.white
            }
        }
    }</code></pre>
<p>그래서 만약 셀을 하나 선택하면 그 셀의 배경은 lightGray로 텍스트 색은 Black으로 변경된다. 이는 여러개의 셀을 선택할때는 구현하지 못하고 하나의 셀만 선택하는 경우에만 가능한 방법일 것이다. </p>
<h2 id="💻-topview">💻 TopView</h2>
<p>스토리보드를 통해 구현한 TopView는 다음과 같다. 
<a href="https://github.com/JiYeonDu/iOS_practice/tree/dev/subway/kioskMiddle">깃허브링크</a>
<img src="https://velog.velcdn.com/images/shisu_shin/post/c0d2bdd5-d0c9-4e5f-9c2b-0ead8c10a98e/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[문풀] pow(2.0,2.0) == 2 ** 2]]></title>
            <link>https://velog.io/@shisu_shin/%EB%AC%B8%ED%92%80-pow22-2-2</link>
            <guid>https://velog.io/@shisu_shin/%EB%AC%B8%ED%92%80-pow22-2-2</guid>
            <pubDate>Thu, 04 Apr 2024 05:38:24 GMT</pubDate>
            <description><![CDATA[<h3 id="⚙️-3진법-뒤집기">⚙️ 3진법 뒤집기</h3>
<p><img src="https://velog.velcdn.com/images/shisu_shin/post/3200aa4c-b916-4589-9402-7e3f8ef00d0d/image.png" alt="">
문제는 단계별로 차근차근 코드를 짜기로 했다. 먼저 10진법을 3진법으로 만들고 앞뒤반전을 한 후 10진법으로 표현할 것이다. 그런데 10진법 수를 3진법으로 변화는 과정에서 리스트를 사용하니까 앞뒤반전하는 과정 없이 리스트에 앞뒤반전한 순서대로 결과가 들어갔다.</p>
<pre><code>var num = n
    var result: [Int] = []
    while num &gt;= 3 {
        result.append(num % 3)
        num = num / 3
    }
    result.append(num)</code></pre><p>위 코드에서 num이 45라면 실행한 후 result 리스트에는 [0, 0, 2, 1]이 들어가 있다. 이제 다시 3진법수를 10진법으로 돌이키는 과정에서 제곱을 활용했다.
<img src="https://velog.velcdn.com/images/shisu_shin/post/9a15162c-3287-468d-8bbe-50eb16826cc5/image.png" alt="">
제곱수를 표현하기 위해 <code>**</code>를 사용했지만 플레이그라운드에서 에러가 났다. <code>**</code> 대신 <code>pow()</code> 메서드를 사용하라는 경고창이 떴다. pow 메서드를 살펴보면 decimal과 Int를 인수로 받고 리턴을 Decimal로 해주고 있다. 
<img src="https://velog.velcdn.com/images/shisu_shin/post/471cfae0-d3fb-4919-aeb4-43e55bd17ee7/image.png" alt="">
그래서 다음과 같이 수정했더니 또 에러가 발생했다.
<img src="https://velog.velcdn.com/images/shisu_shin/post/596121b7-b2e6-4eaf-a723-4c7fd6137467/image.png" alt="">
검색해보니 &quot;Initializer &#39;init(_:)&#39; requires that &#39;Decimal&#39; conform to &#39;BinaryInteger&#39;&quot; 에러는 Decimal 타입이 BinaryInteger 프로토콜을 준수하지 않아서 발생하는 것이라고 한다. BinaryInteger 프로토콜은 이 프로토콜은 정수 타입이 이진 연산을 수행할 수 있도록하는 프로토콜이라고 한다. Double과 Decimal도 다른 타입인데, decimal은 더 정밀한 소수점을 표시하기 위한 타입이라고 생각하면 된다.</p>
<blockquote>
<p>정리하면, Decimal을 바로 Int로 바꿀수는 없다! Double은  Int()로 소수점을 버리고 변환이 가능하다.</p>
</blockquote>
<p>그래서 수정한 결과는 다음과 같다. pow()메서드의 다른 타입을 사용했다.
<img src="https://velog.velcdn.com/images/shisu_shin/post/fa293b45-9c9b-43df-84eb-cf8b1ce83685/image.png" alt=""></p>
<p>이 <a href="https://velog.io/@bibi6666667/Swift-pow-Ambiguous-use-of-pow-%EC%97%90%EB%9F%AC-initializer-init-requires-that-Decimal-conform-to-BinaryInteger-%EC%97%90%EB%9F%AC">블로그</a>를 참고했다.</p>
<p>그렇게 제출한 코드는 다음과 같다.</p>
<pre><code class="language-swift">func solution(_ n:Int) -&gt; Int {
    //3진법으로
    var num = n
    var result: [Int] = []
    while num &gt;= 3 {
        result.append(num % 3)
        num = num / 3
    }
    result.append(num)
    num = 0
    for i in 0...result.count-1 {
        num += Int(pow(3.0,Float(i))) * result[result.count-1-i]
    }
    return 0
}
</code></pre>
<blockquote>
<p>pow(_: Float, _: Float) -&gt; Float</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[문풀] 최대공약수와 최대공배수]]></title>
            <link>https://velog.io/@shisu_shin/%EB%AC%B8%ED%92%80-%EC%B5%9C%EB%8C%80%EA%B3%B5%EC%95%BD%EC%88%98%EC%99%80-%EC%B5%9C%EB%8C%80%EA%B3%B5%EB%B0%B0%EC%88%98</link>
            <guid>https://velog.io/@shisu_shin/%EB%AC%B8%ED%92%80-%EC%B5%9C%EB%8C%80%EA%B3%B5%EC%95%BD%EC%88%98%EC%99%80-%EC%B5%9C%EB%8C%80%EA%B3%B5%EB%B0%B0%EC%88%98</guid>
            <pubDate>Tue, 02 Apr 2024 08:05:43 GMT</pubDate>
            <description><![CDATA[<h3 id="⚙️-최대공약수-최소공배수">⚙️ 최대공약수, 최소공배수</h3>
<p><img src="https://velog.velcdn.com/images/shisu_shin/post/5ed92f8f-cc44-41b7-99e7-bba0e550d848/image.png" alt=""></p>
<p>최대공약수는 입력된 숫자보다 작을 것이므로 반복문을 사용하면 될것이다. 최소공배수도 같은 원리로 입력된 수와 같거나 그 이상일 것이므로 반복문을 사용하면 될것이다. 구현해 보면</p>
<pre><code class="language-swift">func solution(_ n:Int, _ m:Int) -&gt; [Int] {
    //최대공약수
    var min = n &lt; m ? n : m
    while true {
        if n % min == 0 &amp;&amp; m % min == 0{
            break
        }
        min += 1
    }
    //최소공배수
    var max = n &gt; m ? n : m
    while true {
        if max % n == 0 &amp;&amp; max % m == 0{
            break
        }
        max += 1
    }

    return [min, max]
}</code></pre>
<p>이렇게 구현했더니 testCase 2번째에서 최대공약수를 찾지 못하였다. 생각해보니 위의 코드는 두 수가 배수 관계에 있어야지만, 즉 두 수의 최대공약수가 1이 아닐때만 성립하는 코드였다. 그래서 아래와 같이 수정하였다.</p>
<pre><code class="language-swift">func solution(_ n:Int, _ m:Int) -&gt; [Int] {
   var min = n &gt; m ? n : m
    while true {
        if n % min == 0 &amp;&amp; m % min == 0{
            break
        }
        min -= 1
    }
    var max = n &gt; m ? n : m
    while true {
        if max % n == 0 &amp;&amp; max % m == 0{
            break
        }
        max += 1
    }

    return [min, max]
}</code></pre>
<p>이렇게 풀었더니 성공했다. </p>
<p>다른사람들의 풀이를 보니 수학적인 지식을 활용한 풀이가 많았다. </p>
<pre><code class="language-swift">func solution(_ n:Int, _ m:Int) -&gt; [Int] {
    var first: [Int] = []
    for index in 1...n {
        if n % index == 0  &amp;&amp; m % index == 0 {
            first.append(index)
        }
    }
    let maxValue: Int = first[first.count-1]
    return [maxValue ,(n * m)/maxValue ]
}</code></pre>
<p>이 풀이에 의하면 어떤 수의 최소공배수는 두 수의 곱을 최대공약수로 나눈 값이였다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[문풀] 콘솔에 입력받는법]]></title>
            <link>https://velog.io/@shisu_shin/%EB%AC%B8%ED%92%80-%EC%BD%98%EC%86%94%EC%97%90-%EC%9E%85%EB%A0%A5%EB%B0%9B%EB%8A%94%EB%B2%95</link>
            <guid>https://velog.io/@shisu_shin/%EB%AC%B8%ED%92%80-%EC%BD%98%EC%86%94%EC%97%90-%EC%9E%85%EB%A0%A5%EB%B0%9B%EB%8A%94%EB%B2%95</guid>
            <pubDate>Tue, 02 Apr 2024 07:10:46 GMT</pubDate>
            <description><![CDATA[<h3 id="⚙️-직사각형-별찍기">⚙️ 직사각형 별찍기</h3>
<p><img src="https://velog.velcdn.com/images/shisu_shin/post/911e4c17-7a30-43a1-9584-241067a3f86b/image.png" alt=""></p>
<p>해당 문제를 풀기위해 commandLine 파일에 옮겼다. (플레이그라운드에서는 readLine()이 실행이 안된다!!) </p>
<p><img src="https://velog.velcdn.com/images/shisu_shin/post/c977d2a4-96c1-4da1-9ba0-2d972418a488/image.png" alt="">
처음 문제 풀기 전에 답안 영역에 이렇게 작성되어 있는데 readLine()이라는 함수를 사용해본적이 없었다. 함수로 별을 찍는 것은 for문을 통해 단순하게 구현했다. 다만 한줄에 별을 찍고 다음 줄로 넘어가는 것을 구현하려고 할때 처음에는 print<code>(&quot;\n&quot;)</code>처럼 개행문자를 프린트하려고 했으나 이러면 두줄이 띄어지게 되어 <code>print(&quot;&quot;)</code>로 하게 되었다. <strong>print()함수가 인자를 출력한 후 줄바꿈문자를 출력한다는 사실을 이용했다.</strong></p>
<pre><code class="language-swift">let n = readLine()!.components(separatedBy: [&quot; &quot;]).map { Int($0)! }
let (a, b) = (n[0], n[1])
for _ in 1...b {
    for _ in 1...a {
        print(&quot;*&quot;, terminator: &quot;&quot;)
    }
    print(&quot;&quot;)
}</code></pre>
<h3 id="📖-readline">📖 readLine()</h3>
<pre><code class="language-swift">func readline(strippingNewline: Bool = true) -&gt; String?</code></pre>
<p>apple 공식문서에 이 함수의 원형이 나타나있다. 일단 이 함수는 <strong>어떤 입력을 받던 String?타입으로 반환</strong>한다. 예시를 보면 </p>
<pre><code class="language-swift">var a = readLine()
print(a)</code></pre>
<p>를 실행하고 a를 입력하면 출력은 <code>Optional(&quot;a&quot;)</code>이다. 그렇기 때문에 String값을 얻으려면 강제해제 해주거나 <code>readLine()!</code> 암묵적으로 해제해주면 된다. 
그리고 이 함수는 입력의 끝을 개행문자로 인식한다. 즉 한줄씩 입력 받는다. 위의 함수에 입력을</p>
<pre><code>a b c
d e</code></pre><p>로 하면 <code>d e</code>가 입력되기 전에 종료되면 결과는 <code>Optional(&quot;a b c&quot;)</code>이다. </p>
<p>그렇다면 문제의 예시처럼 공백이 있는 입력값을 받아 공백을 제외하고 처리하려면 어떻게 해야할까? 문제에서 미리 예시를 주었듯이 고차함수를 활용하면 된다.</p>
<pre><code class="language-swift">let n = readLine()!.components(separatedBy: [&quot; &quot;]).map { Int($0)! }</code></pre>
<p>먼저 사용자로부터 입력을 한 줄로 읽어드리고, 옵셔널을 강제로 해제한다. 그런다음 <code>.components(separatedBy: [&quot; &quot;])</code>를 통해 문자열을 공백을 기준으로 나누어 문자열 배열로 만든다. 그 후 <code>map { Int($0)! }</code> 로 문자열 배열의 각 요소를 정수로 반환한다.</p>
<blockquote>
<h4 id="정리하자면">정리하자면</h4>
</blockquote>
<ul>
<li>readLine()함수는 한줄씩 입력받고 반환값은 String!</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>