2024. 1. 9. 17:29ㆍSKKU DT
이전 글에 이어서, 셀 목록에 Object Pool을 적용하는 것을 이어서 진행한다. -Self Study
이전 Cell과 CellPool 스크립트를 갈아엎고 LegacyDataManager에서 수정할 방법을 찾아본다.
GPT를 이용하려 했지만 잘 되지 않았다....
강사님의 답
상속 구조로 받아서 쓰게 만들예정이다.
GOTableViewController 스크립트 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public abstract class GOTableViewController : MonoBehaviour
{
//Cell을 생성하기 위한 cell 프리팹
[SerializeField] protected GOTableViewCell cellPrefab; //protected는 상속받은 클래스에서 사용 가능. 어차피 Serialized이기 때문에 public은 있으나 마나.
//Cell을 생성하게 될 content 객체
[SerializeField] protected Transform content;
//Queue 생성
protected Queue<GOTableViewCell> reuseQueue = new Queue<GOTableViewCell>();
//cell의 높이
private float cellHeight;
//전체 셀의 개수
private int totalRows;
//자식 클래스에서 GOTableViewController가 표시해야 할 전체 Data의 수를 반환하는 메서드
protected abstract int numberOfRows(); //추상 메서드 생성, 맨 위의 class도 추상 클래스로 변경(public abstract class GOTableViewController) 자식 클래스가 상속받아서 해당 부분을 채울 것이다. 틀만 만드는 느낌.
//스크롤 바가 보이는 부분에다 맞추면 안되고 전체의 셀에 대한 스크롤 크기를 맞춰야 한다.
protected abstract GOTableViewCell cellForRowAtIndex(int index); //추상 메서드 하나 더 생성.
protected virtual void Start() //virtual 달아주는 이유는 부모 클래스에서 선언되었지만 자식 클래스에서 사용됨. abstract는 부모 클래스에서 내용을 정의할 수 없고 형태만 정의 가능. virtual은 부모에서도 구현 가능.
{
totalRows = this.numberOfRows();
float screenHeight = Screen.height; //화면의 높이 얻어오기. Canvas의 높이가 960으로 설정되어있다.
cellHeight = cellPrefab.GetComponent<RectTransform>().sizeDelta.y; //셀의 높이. 200으로 설정되어있다.
//표시해야 할 cell의 수 계산
int maxRows = (int)screenHeight / (int)cellHeight + 2; //줄 수이기 때문에 int로 줄의 개수를 자연수로 받는다. 한 화면에 표시되는 셀의 개수는 960/200=4.8정도 된다. 아래 가려진 셀의 개수에 여유를 주기 위해 2를 더한다.
maxRows = (maxRows > totalRows) ? totalRows : maxRows;
//전체 셀을 포함하는 높이를 가질 수 도록 content의 크기를 조정
RectTransform contentTransform = content.GetComponent<RectTransform>();
contentTransform.sizeDelta = new Vector2(0, totalRows * cellHeight);
}
}
virtual 달아주는 이유는 부모 클래스에서 선언되었지만 자식 클래스에서 사용됨. abstract는 부모 클래스에서 내용을 정의할 수 없고 형태만 정의 가능. virtual은 부모에서도 구현 가능.
GOTableViewController 스크립트를 상속받는 자식 클래스 LegacyTableViewController 스크립트 생성
public class LegacyTableViewController : GOTableViewController
{
}
GOTableViewController를 상속받는다.
클래스 이름에 오류 표시가 뜨는데, Alt+Enter로 자동 수정을 불러오고 적용하면 추상 메서드를 가져올 수 있다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
public class LegacyTableViewController : GOTableViewController
{
[SerializeField] private string serviceKey; //서버 키 입력
private List<Legacy> legacyList = new List<Legacy>(); //리스트 생성
protected void Start()
{
StartCoroutine(LoadData());
}
protected override int numberOfRows()
{
return legacyList.Count; //개수 반환
}
protected override GOTableViewCell cellForRowAtIndex(int index)
{
return null;//특정한 index의 셀을 만들려고할 때 특정한 셀을 만들어서 반환한다.
}
//이전 LegacyDataManger에서 LoadData 부분을 가져온다. 더보기 버튼 관련 코드는 지운다.
IEnumerator LoadData()
{
string url = string.Format("{0}?page={1}&perPage={2}&serviceKey={3}", Constants.url, 0, 100, serviceKey);
UnityWebRequest request = new UnityWebRequest();
using (request = UnityWebRequest.Get(url))
{
yield return request.SendWebRequest();
if (request.result != UnityWebRequest.Result.Success)
{
Debug.Log(request.error);
}
else
{
string result = request.downloadHandler.text; //텍스트 형식으로 받아오기. 많이 불러왔었던 빼곡한 글들로 불러온다.
LegacyData legacyData = JsonUtility.FromJson<LegacyData>(result); //파싱에서 가져온 이름 LegacyData
Legacy[] legacyArray = legacyData.data;
for (int i = 0; i < legacyArray.Length; i++)
{
legacyList.Add(legacyArray[i]);
}
}
base.Start();
}
}
}
Content에 Vertical Layout Group과 Content Size Fitter 지우기. 스크립트로 조절할 예정
LegacyCell 프리팹을 복사해서 LegacyTableViewCell 만들기
기존에 들어있던 스크립트 지우고 LEgacyTableViewCell 추가
CellPrefab 자리에 LegacyTableViewCell 끌어다 넣기
서비스 키 넣기
Content의 높이가 셀의 개수에 맞게 엄청 길어져있으면 된다.
GOTableViewCell 스크립트를 상속받는 자식 클래스 LegacyTableViewCell 스크립트 생성
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
public class LegacyTableViewCell : GOTableViewCell
{
[SerializeField] TMP_Text legacyName;
[SerializeField] TMP_Text type;
[SerializeField] TMP_Text designatedDate;
}
GOTableViewController 스크립트 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public abstract class GOTableViewController : MonoBehaviour
{
//Cell을 생성하기 위한 cell 프리팹
[SerializeField] protected GOTableViewCell cellPrefab; //protected는 상속받은 클래스에서 사용 가능. 어차피 Serialized이기 때문에 public은 있으나 마나.
//Cell을 생성하게 될 content 객체
[SerializeField] protected Transform content;
//Queue 생성
protected Queue<GOTableViewCell> reuseQueue = new Queue<GOTableViewCell>();
//cell의 높이
private float cellHeight;
//전체 셀의 개수
private int totalRows;
//자식 클래스에서 GOTableViewController가 표시해야 할 전체 Data의 수를 반환하는 메서드
protected abstract int numberOfRows(); //추상 메서드 생성, 맨 위의 class도 추상 클래스로 변경(public abstract class GOTableViewController) 자식 클래스가 상속받아서 해당 부분을 채울 것이다. 틀만 만드는 느낌.
//스크롤 바가 보이는 부분에다 맞추면 안되고 전체의 셀에 대한 스크롤 크기를 맞춰야 한다.
protected abstract GOTableViewCell cellForRowAtIndex(int index); //추상 메서드 하나 더 생성.
protected virtual void Start() //virtual 달아주는 이유는 부모 클래스에서 선언되었지만 자식 클래스에서 사용됨. abstract는 부모 클래스에서 내용을 정의할 수 없고 형태만 정의 가능. virtual은 부모에서도 구현 가능.
{
totalRows = this.numberOfRows();
float screenHeight = Screen.height; //화면의 높이 얻어오기. Canvas의 높이가 960으로 설정되어있다.
cellHeight = cellPrefab.GetComponent<RectTransform>().sizeDelta.y; //셀의 높이. 200으로 설정되어있다.
//표시해야 할 cell의 수 계산
int maxRows = (int)screenHeight / (int)cellHeight + 2; //줄 수이기 때문에 int로 줄의 개수를 자연수로 받는다. 한 화면에 표시되는 셀의 개수는 960/200=4.8정도 된다. 아래 가려진 셀의 개수에 여유를 주기 위해 2를 더한다.
maxRows = (maxRows > totalRows) ? totalRows : maxRows;
//전체 셀을 포함하는 높이를 가질 수 도록 content의 크기를 조정
RectTransform contentTransform = content.GetComponent<RectTransform>();
contentTransform.sizeDelta = new Vector2(0, totalRows * cellHeight);
}
protected GOTableViewCell dequeueReuseableCell() //재사용이 가능한 Cell을 반환해주는 메서드. 재사용 cell 혹은 mull이 반환된다.
{
if(reuseQueue.Count > 0) //쓸만한 셀이 있다면
{
GOTableViewCell cell = reuseQueue.Dequeue();
cell.gameObject.SetActive(true);
return cell;
}
else
{
return null; //재사용 가능한 셀이 있다면 Instantiate 할 수 있다고 생각할 수 있는데, 여기서 자식 형태의 cell을 만들어낼 수는 없다. 자식 쪽에서 만들어야 함. 넣었던 자식이 그대로 반환될 것.
}
}
}
LegacyTableViewController 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
public class LegacyTableViewController : GOTableViewController
{
[SerializeField] private string serviceKey; //서버 키 입력
private List<Legacy> legacyList = new List<Legacy>(); //리스트 생성
protected void Start()
{
StartCoroutine(LoadData());
}
protected override int numberOfRows()
{
return legacyList.Count; //개수 반환
}
protected override GOTableViewCell cellForRowAtIndex(int index)
{
//특정한 index의 셀을 만들려고할 때 특정한 셀을 만들어서 반환한다.
//부모에 만들어 놓으면, 상속만 받으면 가져다 쓰기만 하면 된다. 셀을 생성 하는 건 자식이 하더라도 셀을 재사용할 수 있는 로직은 GOTableViewController에서 만든다.
LegacyTableViewCell cell = dequeueReuseableCell() as LegacyTableViewCell; //재사용 가능한 셀이 있는가 as로 변환을 시킨다. 메서드의 반환 값이 GOTableViewCell이다.
if(cell == null)
{
cell = Instantiate(cellPrefab, content) as LegacyTableViewCell; //변환
}
return null;
}
//이전 LegacyDataManger에서 LoadData 부분을 가져온다. 더보기 버튼 관련 코드는 지운다.
IEnumerator LoadData()
{
string url = string.Format("{0}?page={1}&perPage={2}&serviceKey={3}", Constants.url, 0, 100, serviceKey);
UnityWebRequest request = new UnityWebRequest();
using (request = UnityWebRequest.Get(url))
{
yield return request.SendWebRequest();
if (request.result != UnityWebRequest.Result.Success)
{
Debug.Log(request.error);
}
else
{
string result = request.downloadHandler.text; //텍스트 형식으로 받아오기. 많이 불러왔었던 빼곡한 글들로 불러온다.
LegacyData legacyData = JsonUtility.FromJson<LegacyData>(result); //파싱에서 가져온 이름 LegacyData
Legacy[] legacyArray = legacyData.data;
for (int i = 0; i < legacyArray.Length; i++)
{
legacyList.Add(legacyArray[i]);
}
}
base.Start();
}
}
}
LegacyTableViewCell 스크립트 수정 -public으로 수정. 그래야 LegacyTableViewController에서 cell.~~을 쓸 수 있다.
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
public class LegacyTableViewCell : GOTableViewCell
{
public TMP_Text legacyName;
public TMP_Text type;
public TMP_Text designatedDate;
}
LegacyTableViewController 스크립트 수정
cell.legacyName.text = legacyList[index].명칭;
cell.type.text = legacyList[index].종목;
cell.designatedDate.text = legacyList[index].지정일;
위의 내용 추가, 아래 전체 코드
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
public class LegacyTableViewController : GOTableViewController
{
[SerializeField] private string serviceKey; //서버 키 입력
private List<Legacy> legacyList = new List<Legacy>(); //리스트 생성
protected void Start()
{
StartCoroutine(LoadData());
}
protected override int numberOfRows()
{
return legacyList.Count; //개수 반환
}
protected override GOTableViewCell cellForRowAtIndex(int index)
{
//특정한 index의 셀을 만들려고할 때 특정한 셀을 만들어서 반환한다.
//부모에 만들어 놓으면, 상속만 받으면 가져다 쓰기만 하면 된다. 셀을 생성 하는 건 자식이 하더라도 셀을 재사용할 수 있는 로직은 GOTableViewController에서 만든다.
LegacyTableViewCell cell = dequeueReuseableCell() as LegacyTableViewCell; //재사용 가능한 셀이 있는가 as로 변환을 시킨다. 메서드의 반환 값이 GOTableViewCell이다.
if(cell == null)
{
cell = Instantiate(cellPrefab, content) as LegacyTableViewCell; //변환
}
cell.legacyName.text = legacyList[index].명칭;
cell.type.text = legacyList[index].종목;
cell.designatedDate.text = legacyList[index].지정일;
return cell;
}
//이전 LegacyDataManger에서 LoadData 부분을 가져온다. 더보기 버튼 관련 코드는 지운다.
IEnumerator LoadData()
{
string url = string.Format("{0}?page={1}&perPage={2}&serviceKey={3}", Constants.url, 0, 100, serviceKey);
UnityWebRequest request = new UnityWebRequest();
using (request = UnityWebRequest.Get(url))
{
yield return request.SendWebRequest();
if (request.result != UnityWebRequest.Result.Success)
{
Debug.Log(request.error);
}
else
{
string result = request.downloadHandler.text; //텍스트 형식으로 받아오기. 많이 불러왔었던 빼곡한 글들로 불러온다.
LegacyData legacyData = JsonUtility.FromJson<LegacyData>(result); //파싱에서 가져온 이름 LegacyData
Legacy[] legacyArray = legacyData.data;
for (int i = 0; i < legacyArray.Length; i++)
{
legacyList.Add(legacyArray[i]);
}
}
base.Start();
}
}
}
아직 재사용 가능한 셀을 Queue에 넣는 작업은 하지 않았다.
GOTableViewController 스크립트 수정 (수정 내용은 WinMerge로 이전 것과 비교하면 편하다)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public abstract class GOTableViewController : MonoBehaviour
{
//Cell을 생성하기 위한 cell 프리팹
[SerializeField] protected GOTableViewCell cellPrefab; //protected는 상속받은 클래스에서 사용 가능. 어차피 Serialized이기 때문에 public은 있으나 마나.
//Cell을 생성하게 될 content 객체
[SerializeField] protected Transform content;
//Queue 생성
protected Queue<GOTableViewCell> reuseQueue = new Queue<GOTableViewCell>();
//cell의 높이
private float cellHeight;
//전체 셀의 개수
private int totalRows;
//자식 클래스에서 GOTableViewController가 표시해야 할 전체 Data의 수를 반환하는 메서드
protected abstract int numberOfRows(); //추상 메서드 생성, 맨 위의 class도 추상 클래스로 변경(public abstract class GOTableViewController) 자식 클래스가 상속받아서 해당 부분을 채울 것이다. 틀만 만드는 느낌.
//스크롤 바가 보이는 부분에다 맞추면 안되고 전체의 셀에 대한 스크롤 크기를 맞춰야 한다.
protected abstract GOTableViewCell cellForRowAtIndex(int index); //추상 메서드 하나 더 생성.
protected virtual void Start() //virtual 달아주는 이유는 부모 클래스에서 선언되었지만 자식 클래스에서 사용됨. abstract는 부모 클래스에서 내용을 정의할 수 없고 형태만 정의 가능. virtual은 부모에서도 구현 가능.
{
totalRows = this.numberOfRows(); //전체 몇 개의 값이 필요한지 얻어온다.
float screenHeight = Screen.height; //화면의 높이 얻어오기. Canvas의 높이가 960으로 설정되어있다.
cellHeight = cellPrefab.GetComponent<RectTransform>().sizeDelta.y; //셀의 높이. 200으로 설정되어있다.
//표시해야 할 cell의 수 계산
int maxRows = (int)screenHeight / (int)cellHeight + 2; //줄 수이기 때문에 int로 줄의 개수를 자연수로 받는다. 한 화면에 표시되는 셀의 개수는 960/200=4.8정도 된다. 아래 가려진 셀의 개수에 여유를 주기 위해 2를 더한다.
maxRows = (maxRows > totalRows) ? totalRows : maxRows;
//전체 셀을 포함하는 높이를 가질 수 도록 content의 크기를 조정
RectTransform contentTransform = content.GetComponent<RectTransform>();
contentTransform.sizeDelta = new Vector2(0, totalRows * cellHeight);
//초기 Cell을 생성
for(int i = 0; i < maxRows; i++)
{
GOTableViewCell cell = cellForRowAtIndex(i);//인덱스만 넣으면 셀이 만들어지도록.
cell.gameObject.transform.localPosition = new Vector3(0, -i * cellHeight, 0); //첫 번째 셀 높이는 0, 두 번째 셀 높이는 -1 * 200 = -200
}
}
protected GOTableViewCell dequeueReuseableCell() //재사용이 가능한 Cell을 반환해주는 메서드. 재사용 cell 혹은 null이 반환된다.
{
if(reuseQueue.Count > 0) //쓸만한 셀이 있다면
{
GOTableViewCell cell = reuseQueue.Dequeue();
cell.gameObject.SetActive(true);
return cell;
}
else
{
return null; //재사용 가능한 셀이 있다면 Instantiate 할 수 있다고 생각할 수 있는데, 여기서 자식 형태의 cell을 만들어낼 수는 없다. 자식 쪽에서 만들어야 함. 넣었던 자식이 그대로 반환될 것.
}
}
}
아직 잘 나오지는 않는다. Content의 피봇이 왼쪽에 있다...
피봇 x 값을 0.5로 설정하니 잘 나온다.
Scroll View의 OnValueChange에 넣을 함수를 GOTableViewController에 아래에 만든다.
public void OnValueChanged(Vector2 vector)
{
Debug.Log(vector);
}
가장 아래로 내려가면 x, y가 0이 된다.
다른 값을 Debug.Log 하면 아래와 같다.
public void OnValueChanged(Vector2 vector)
{
Debug.Log($"Screen.height: {Screen.height}, Content.localPosition.y: {content.localPosition.y}");
이제 재사용 가능한 셀을 만들어야 한다. First Cell과 Last Cell의 상태를 보고 추가할지 지울지를 판단한다. LinkedList가 필요한 상황.
GOTableViewController 스크립트 수정 -LinkedList<> 생성
//Cell 간의 연결고리를 관리하기 위한 List
protected LinkedList<GOTableViewCell> cellLinkedList = new LinkedList<GOTableViewCell>();
//초기 Cell을 생성
for(int i = 0; i < maxRows; i++)
{
GOTableViewCell cell = cellForRowAtIndex(i);//인덱스만 넣으면 셀이 만들어지도록.
cell.gameObject.transform.localPosition = new Vector3(0, -i * cellHeight, 0); //첫 번째 셀 높이는 0, 두 번째 셀 높이는 -1 * 200 = -200
//LinkedList에 Last Cell로 추가
cellLinkedList.AddLast(cell);
}
LinkedList를 생성하고 AddLast로 추가된다.
전체 코
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public abstract class GOTableViewController : MonoBehaviour
{
//Cell을 생성하기 위한 cell 프리팹
[SerializeField] protected GOTableViewCell cellPrefab; //protected는 상속받은 클래스에서 사용 가능. 어차피 Serialized이기 때문에 public은 있으나 마나.
//Cell을 생성하게 될 content 객체
[SerializeField] protected Transform content;
//Cell 재사용을 위한 Queue 생성
protected Queue<GOTableViewCell> reuseQueue = new Queue<GOTableViewCell>();
//Cell 간의 연결고리를 관리하기 위한 List
protected LinkedList<GOTableViewCell> cellLinkedList = new LinkedList<GOTableViewCell>();
//cell의 높이
private float cellHeight;
//전체 셀의 개수
private int totalRows;
//자식 클래스에서 GOTableViewController가 표시해야 할 전체 Data의 수를 반환하는 메서드
protected abstract int numberOfRows(); //추상 메서드 생성, 맨 위의 class도 추상 클래스로 변경(public abstract class GOTableViewController) 자식 클래스가 상속받아서 해당 부분을 채울 것이다. 틀만 만드는 느낌.
//스크롤 바가 보이는 부분에다 맞추면 안되고 전체의 셀에 대한 스크롤 크기를 맞춰야 한다.
protected abstract GOTableViewCell cellForRowAtIndex(int index); //추상 메서드 하나 더 생성.
protected virtual void Start() //virtual 달아주는 이유는 부모 클래스에서 선언되었지만 자식 클래스에서 사용됨. abstract는 부모 클래스에서 내용을 정의할 수 없고 형태만 정의 가능. virtual은 부모에서도 구현 가능.
{
totalRows = this.numberOfRows(); //전체 몇 개의 값이 필요한지 얻어온다.
float screenHeight = Screen.height; //화면의 높이 얻어오기. Canvas의 높이가 960으로 설정되어있다.
cellHeight = cellPrefab.GetComponent<RectTransform>().sizeDelta.y; //셀의 높이. 200으로 설정되어있다.
//표시해야 할 cell의 수 계산
int maxRows = (int)screenHeight / (int)cellHeight + 2; //줄 수이기 때문에 int로 줄의 개수를 자연수로 받는다. 한 화면에 표시되는 셀의 개수는 960/200=4.8정도 된다. 아래 가려진 셀의 개수에 여유를 주기 위해 2를 더한다.
maxRows = (maxRows > totalRows) ? totalRows : maxRows;
//전체 셀을 포함하는 높이를 가질 수 도록 content의 크기를 조정
RectTransform contentTransform = content.GetComponent<RectTransform>();
contentTransform.sizeDelta = new Vector2(0, totalRows * cellHeight);
//초기 Cell을 생성
for(int i = 0; i < maxRows; i++)
{
GOTableViewCell cell = cellForRowAtIndex(i);//인덱스만 넣으면 셀이 만들어지도록.
cell.gameObject.transform.localPosition = new Vector3(0, -i * cellHeight, 0); //첫 번째 셀 높이는 0, 두 번째 셀 높이는 -1 * 200 = -200
//LinkedList에 Last Cell로 추가
cellLinkedList.AddLast(cell);
}
}
protected GOTableViewCell dequeueReuseableCell() //재사용이 가능한 Cell을 반환해주는 메서드. 재사용 cell 혹은 null이 반환된다.
{
if(reuseQueue.Count > 0) //쓸만한 셀이 있다면
{
GOTableViewCell cell = reuseQueue.Dequeue();
cell.gameObject.SetActive(true);
return cell;
}
else
{
return null; //재사용 가능한 셀이 있다면 Instantiate 할 수 있다고 생각할 수 있는데, 여기서 자식 형태의 cell을 만들어낼 수는 없다. 자식 쪽에서 만들어야 함. 넣었던 자식이 그대로 반환될 것.
}
}
public void OnValueChanged(Vector2 vector)
{
Debug.Log($"Screen.height: {Screen.height}, Content.localPosition.y: {content.localPosition.y}");
}
}
GOTableViewCell 스크립트 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GOTableViewCell : MonoBehaviour
{
public int Index;
}
Index가 추가되었다.
GOTableViewController 스크립트 수정 -OnValueChanged 함수 작성
public void OnValueChanged(Vector2 vector)
{
Debug.Log($"Screen.height: {Screen.height}, Content.localPosition.y: {content.localPosition.y}");
if (content.localPosition.y + Screen.height > cellLinkedList.Last.Value.Index * cellHeight + cellHeight) // 부등호 왼쪽은 현재 위치 + 스크린 높이, 부등호 오른쪽은 마지막 보이는 셀의 개수 * 셀의 높이 + 여유 높이 하나 추가. 스크롤을 내려서 스크린 높이가 커지면,
{
//하단에 새로운 Cell이 만들어지는 상황
//처음에 있던 Cell은 Reuse Queue에 저장
LinkedListNode<GOTableViewCell> firstCellNode = cellLinkedList.First; //첫 번째 셀은 이미 화면을 벗어나서 없을 테니까.
cellLinkedList.RemoveFirst(); //첫 번째 있던 값을 지워버리기
firstCellNode.Value.gameObject.SetActive(false); //첫 번째 있던 셀을 비활성화하기
reuseQueue.Enqueue(firstCellNode.Value); //reuseQueue에 저장하기
//하단에 새로운 Cell 생성
LinkedListNode<GOTableViewCell> lastCellNode = cellLinkedList.Last;
int currentIndex = lastCellNode.Value.Index;
GOTableViewCell cell = cellForRowAtIndex(currentIndex + 1);
cell.gameObject.transform.localPosition = new Vector3(0, -(currentIndex + 1) * cellHeight, 0); //좌표를 마지막으로 보낸다.
cellLinkedList.AddAfter(lastCellNode, cell);
cell.gameObject.transform.SetAsLastSibling(); //게임 오브젝트 순서도 마지막으로 보낸다.
}
else if (content.localPosition.y < cellLinkedList.First.Value.Index * cellHeight)
{
//상단에 새로운 Cell이 만들어지는 상황
//마지막에 있던 Cell은 Reuse Queue에 저장
LinkedListNode<GOTableViewCell> lastCellNode = cellLinkedList.Last;
cellLinkedList.RemoveLast();
lastCellNode.Value.gameObject.SetActive(false);
reuseQueue.Enqueue(lastCellNode.Value);
//상단에 새로운 Cell 생성
LinkedListNode<GOTableViewCell> firstCellNode = cellLinkedList.First;
int currentIndex = firstCellNode.Value.Index;
GOTableViewCell cell = cellForRowAtIndex(currentIndex - 1);
cell.gameObject.transform.localPosition = new Vector3(0, -(currentIndex - 1) * cellHeight, 0);
cellLinkedList.AddBefore(firstCellNode, cell);
cell.gameObject.transform.SetAsFirstSibling();
}
}
전체 코드
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public abstract class GOTableViewController : MonoBehaviour
{
//Cell을 생성하기 위한 cell 프리팹
[SerializeField] protected GOTableViewCell cellPrefab; //protected는 상속받은 클래스에서 사용 가능. 어차피 Serialized이기 때문에 public은 있으나 마나.
//Cell을 생성하게 될 content 객체
[SerializeField] protected Transform content;
//Cell 재사용을 위한 Queue 생성
protected Queue<GOTableViewCell> reuseQueue = new Queue<GOTableViewCell>();
//Cell 간의 연결고리를 관리하기 위한 List
protected LinkedList<GOTableViewCell> cellLinkedList = new LinkedList<GOTableViewCell>();
//cell의 높이
private float cellHeight;
//전체 셀의 개수
private int totalRows;
//자식 클래스에서 GOTableViewController가 표시해야 할 전체 Data의 수를 반환하는 메서드
protected abstract int numberOfRows(); //추상 메서드 생성, 맨 위의 class도 추상 클래스로 변경(public abstract class GOTableViewController) 자식 클래스가 상속받아서 해당 부분을 채울 것이다. 틀만 만드는 느낌.
//스크롤 바가 보이는 부분에다 맞추면 안되고 전체의 셀에 대한 스크롤 크기를 맞춰야 한다.
protected abstract GOTableViewCell cellForRowAtIndex(int index); //추상 메서드 하나 더 생성.
protected virtual void Start() //virtual 달아주는 이유는 부모 클래스에서 선언되었지만 자식 클래스에서 사용됨. abstract는 부모 클래스에서 내용을 정의할 수 없고 형태만 정의 가능. virtual은 부모에서도 구현 가능.
{
totalRows = this.numberOfRows(); //전체 몇 개의 값이 필요한지 얻어온다.
float screenHeight = Screen.height; //화면의 높이 얻어오기. Canvas의 높이가 960으로 설정되어있다.
cellHeight = cellPrefab.GetComponent<RectTransform>().sizeDelta.y; //셀의 높이. 200으로 설정되어있다.
//표시해야 할 cell의 수 계산
int maxRows = (int)screenHeight / (int)cellHeight + 2; //줄 수이기 때문에 int로 줄의 개수를 자연수로 받는다. 한 화면에 표시되는 셀의 개수는 960/200=4.8정도 된다. 아래 가려진 셀의 개수에 여유를 주기 위해 2를 더한다.
maxRows = (maxRows > totalRows) ? totalRows : maxRows;
//전체 셀을 포함하는 높이를 가질 수 도록 content의 크기를 조정
RectTransform contentTransform = content.GetComponent<RectTransform>();
contentTransform.sizeDelta = new Vector2(0, totalRows * cellHeight);
//초기 Cell을 생성
for(int i = 0; i < maxRows; i++)
{
GOTableViewCell cell = cellForRowAtIndex(i);//인덱스만 넣으면 셀이 만들어지도록.
cell.gameObject.transform.localPosition = new Vector3(0, -i * cellHeight, 0); //첫 번째 셀 높이는 0, 두 번째 셀 높이는 -1 * 200 = -200
//LinkedList에 Last Cell로 추가
cellLinkedList.AddLast(cell);
}
}
protected GOTableViewCell dequeueReuseableCell() //재사용이 가능한 Cell을 반환해주는 메서드. 재사용 cell 혹은 null이 반환된다.
{
if(reuseQueue.Count > 0) //쓸만한 셀이 있다면
{
GOTableViewCell cell = reuseQueue.Dequeue();
cell.gameObject.SetActive(true);
return cell;
}
else
{
return null; //재사용 가능한 셀이 있다면 Instantiate 할 수 있다고 생각할 수 있는데, 여기서 자식 형태의 cell을 만들어낼 수는 없다. 자식 쪽에서 만들어야 함. 넣었던 자식이 그대로 반환될 것.
}
}
public void OnValueChanged(Vector2 vector)
{
Debug.Log($"Screen.height: {Screen.height}, Content.localPosition.y: {content.localPosition.y}");
if (content.localPosition.y + Screen.height > cellLinkedList.Last.Value.Index * cellHeight + cellHeight) // 부등호 왼쪽은 현재 위치 + 스크린 높이, 부등호 오른쪽은 마지막 보이는 셀의 개수 * 셀의 높이 + 여유 높이 하나 추가. 스크롤을 내려서 스크린 높이가 커지면,
{
//하단에 새로운 Cell이 만들어지는 상황
//처음에 있던 Cell은 Reuse Queue에 저장
LinkedListNode<GOTableViewCell> firstCellNode = cellLinkedList.First; //첫 번째 셀은 이미 화면을 벗어나서 없을 테니까.
cellLinkedList.RemoveFirst(); //첫 번째 있던 값을 지워버리기
firstCellNode.Value.gameObject.SetActive(false); //첫 번째 있던 셀을 비활성화하기
reuseQueue.Enqueue(firstCellNode.Value); //reuseQueue에 저장하기
//하단에 새로운 Cell 생성
LinkedListNode<GOTableViewCell> lastCellNode = cellLinkedList.Last;
int currentIndex = lastCellNode.Value.Index;
GOTableViewCell cell = cellForRowAtIndex(currentIndex + 1);
cell.gameObject.transform.localPosition = new Vector3(0, -(currentIndex + 1) * cellHeight, 0); //좌표를 마지막으로 보낸다.
cellLinkedList.AddAfter(lastCellNode, cell);
cell.gameObject.transform.SetAsLastSibling(); //게임 오브젝트 순서도 마지막으로 보낸다.
}
else if (content.localPosition.y < cellLinkedList.First.Value.Index * cellHeight)
{
//상단에 새로운 Cell이 만들어지는 상황
//마지막에 있던 Cell은 Reuse Queue에 저장
LinkedListNode<GOTableViewCell> lastCellNode = cellLinkedList.Last;
cellLinkedList.RemoveLast();
lastCellNode.Value.gameObject.SetActive(false);
reuseQueue.Enqueue(lastCellNode.Value);
//상단에 새로운 Cell 생성
LinkedListNode<GOTableViewCell> firstCellNode = cellLinkedList.First;
int currentIndex = firstCellNode.Value.Index;
GOTableViewCell cell = cellForRowAtIndex(currentIndex - 1);
cell.gameObject.transform.localPosition = new Vector3(0, -(currentIndex - 1) * cellHeight, 0);
cellLinkedList.AddBefore(firstCellNode, cell);
cell.gameObject.transform.SetAsFirstSibling();
}
}
}
아직 오류가 많다.
LegacyTableViewController 스크립트에 한 줄 추가
cell.Index = index;
전체 코드
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
public class LegacyTableViewController : GOTableViewController
{
[SerializeField] private string serviceKey; //서버 키 입력
private List<Legacy> legacyList = new List<Legacy>(); //리스트 생성
protected void Start()
{
StartCoroutine(LoadData());
}
protected override int numberOfRows()
{
return legacyList.Count; //개수 반환
}
protected override GOTableViewCell cellForRowAtIndex(int index)
{
//특정한 index의 셀을 만들려고할 때 특정한 셀을 만들어서 반환한다.
//부모에 만들어 놓으면, 상속만 받으면 가져다 쓰기만 하면 된다. 셀을 생성 하는 건 자식이 하더라도 셀을 재사용할 수 있는 로직은 GOTableViewController에서 만든다.
LegacyTableViewCell cell = dequeueReuseableCell() as LegacyTableViewCell; //재사용 가능한 셀이 있는가 as로 변환을 시킨다. 메서드의 반환 값이 GOTableViewCell이다.
if(cell == null)
{
cell = Instantiate(cellPrefab, content) as LegacyTableViewCell; //변환
}
cell.Index = index;
cell.legacyName.text = legacyList[index].명칭;
cell.type.text = legacyList[index].종목;
cell.designatedDate.text = legacyList[index].지정일;
return cell;
}
//이전 LegacyDataManger에서 LoadData 부분을 가져온다. 더보기 버튼 관련 코드는 지운다.
IEnumerator LoadData()
{
string url = string.Format("{0}?page={1}&perPage={2}&serviceKey={3}", Constants.url, 0, 100, serviceKey);
UnityWebRequest request = new UnityWebRequest();
using (request = UnityWebRequest.Get(url))
{
yield return request.SendWebRequest();
if (request.result != UnityWebRequest.Result.Success)
{
Debug.Log(request.error);
}
else
{
string result = request.downloadHandler.text; //텍스트 형식으로 받아오기. 많이 불러왔었던 빼곡한 글들로 불러온다.
LegacyData legacyData = JsonUtility.FromJson<LegacyData>(result); //파싱에서 가져온 이름 LegacyData
Legacy[] legacyArray = legacyData.data;
for (int i = 0; i < legacyArray.Length; i++)
{
legacyList.Add(legacyArray[i]);
}
}
base.Start();
}
}
}
내려갈때 계속 셀이 생성되어 잘 작동한다. 하지만 맨위/맨끝으로 가면 오류가 난다.
끝단에 가면 동작하지 않고 하나 전까지만 동작하게 조건을 추가한다.
GOTableViewController 스크립트 수정
if ((cellLinkedList.Last.Value.Index < totalRows - 1) && (content.localPosition.y + Screen.height > cellLinkedList.Last.Value.Index * cellHeight + cellHeight))
{
}
else if ((cellLinkedList.First.Value.Index > 0) && (content.localPosition.y < cellLinkedList.First.Value.Index * cellHeight))
{
}
전체 코드
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public abstract class GOTableViewController : MonoBehaviour
{
//Cell을 생성하기 위한 cell 프리팹
[SerializeField] protected GOTableViewCell cellPrefab; //protected는 상속받은 클래스에서 사용 가능. 어차피 Serialized이기 때문에 public은 있으나 마나.
//Cell을 생성하게 될 content 객체
[SerializeField] protected Transform content;
//Cell 재사용을 위한 Queue 생성
protected Queue<GOTableViewCell> reuseQueue = new Queue<GOTableViewCell>();
//Cell 간의 연결고리를 관리하기 위한 List
protected LinkedList<GOTableViewCell> cellLinkedList = new LinkedList<GOTableViewCell>();
//cell의 높이
private float cellHeight;
//전체 셀의 개수
private int totalRows;
//자식 클래스에서 GOTableViewController가 표시해야 할 전체 Data의 수를 반환하는 메서드
protected abstract int numberOfRows(); //추상 메서드 생성, 맨 위의 class도 추상 클래스로 변경(public abstract class GOTableViewController) 자식 클래스가 상속받아서 해당 부분을 채울 것이다. 틀만 만드는 느낌.
//스크롤 바가 보이는 부분에다 맞추면 안되고 전체의 셀에 대한 스크롤 크기를 맞춰야 한다.
protected abstract GOTableViewCell cellForRowAtIndex(int index); //추상 메서드 하나 더 생성.
protected virtual void Start() //virtual 달아주는 이유는 부모 클래스에서 선언되었지만 자식 클래스에서 사용됨. abstract는 부모 클래스에서 내용을 정의할 수 없고 형태만 정의 가능. virtual은 부모에서도 구현 가능.
{
totalRows = this.numberOfRows(); //전체 몇 개의 값이 필요한지 얻어온다.
float screenHeight = Screen.height; //화면의 높이 얻어오기. Canvas의 높이가 960으로 설정되어있다.
cellHeight = cellPrefab.GetComponent<RectTransform>().sizeDelta.y; //셀의 높이. 200으로 설정되어있다.
//표시해야 할 cell의 수 계산
int maxRows = (int)screenHeight / (int)cellHeight + 2; //줄 수이기 때문에 int로 줄의 개수를 자연수로 받는다. 한 화면에 표시되는 셀의 개수는 960/200=4.8정도 된다. 아래 가려진 셀의 개수에 여유를 주기 위해 2를 더한다.
maxRows = (maxRows > totalRows) ? totalRows : maxRows;
//전체 셀을 포함하는 높이를 가질 수 도록 content의 크기를 조정
RectTransform contentTransform = content.GetComponent<RectTransform>();
contentTransform.sizeDelta = new Vector2(0, totalRows * cellHeight);
//초기 Cell을 생성
for(int i = 0; i < maxRows; i++)
{
GOTableViewCell cell = cellForRowAtIndex(i);//인덱스만 넣으면 셀이 만들어지도록.
cell.gameObject.transform.localPosition = new Vector3(0, -i * cellHeight, 0); //첫 번째 셀 높이는 0, 두 번째 셀 높이는 -1 * 200 = -200
//LinkedList에 Last Cell로 추가
cellLinkedList.AddLast(cell);
}
}
protected GOTableViewCell dequeueReuseableCell() //재사용이 가능한 Cell을 반환해주는 메서드. 재사용 cell 혹은 null이 반환된다.
{
if(reuseQueue.Count > 0) //쓸만한 셀이 있다면
{
GOTableViewCell cell = reuseQueue.Dequeue();
cell.gameObject.SetActive(true);
return cell;
}
else
{
return null; //재사용 가능한 셀이 있다면 Instantiate 할 수 있다고 생각할 수 있는데, 여기서 자식 형태의 cell을 만들어낼 수는 없다. 자식 쪽에서 만들어야 함. 넣었던 자식이 그대로 반환될 것.
}
}
public void OnValueChanged(Vector2 vector)
{
Debug.Log($"Screen.height: {Screen.height}, Content.localPosition.y: {content.localPosition.y}");
if ((cellLinkedList.Last.Value.Index < totalRows - 1) && (content.localPosition.y + Screen.height > cellLinkedList.Last.Value.Index * cellHeight + cellHeight)) // 부등호 왼쪽은 현재 위치 + 스크린 높이, 부등호 오른쪽은 마지막 보이는 셀의 개수 * 셀의 높이 + 여유 높이 하나 추가. 스크롤을 내려서 스크린 높이가 커지면,
{
//하단에 새로운 Cell이 만들어지는 상황
//처음에 있던 Cell은 Reuse Queue에 저장
LinkedListNode<GOTableViewCell> firstCellNode = cellLinkedList.First; //첫 번째 셀은 이미 화면을 벗어나서 없을 테니까.
cellLinkedList.RemoveFirst(); //첫 번째 있던 값을 지워버리기
firstCellNode.Value.gameObject.SetActive(false); //첫 번째 있던 셀을 비활성화하기
reuseQueue.Enqueue(firstCellNode.Value); //reuseQueue에 저장하기
//하단에 새로운 Cell 생성
LinkedListNode<GOTableViewCell> lastCellNode = cellLinkedList.Last;
int currentIndex = lastCellNode.Value.Index;
GOTableViewCell cell = cellForRowAtIndex(currentIndex + 1);
cell.gameObject.transform.localPosition = new Vector3(0, -(currentIndex + 1) * cellHeight, 0); //좌표를 마지막으로 보낸다.
cellLinkedList.AddAfter(lastCellNode, cell);
cell.gameObject.transform.SetAsLastSibling(); //게임 오브젝트 순서도 마지막으로 보낸다.
}
else if ((cellLinkedList.First.Value.Index > 0) && (content.localPosition.y < cellLinkedList.First.Value.Index * cellHeight))
{
//상단에 새로운 Cell이 만들어지는 상황
//마지막에 있던 Cell은 Reuse Queue에 저장
LinkedListNode<GOTableViewCell> lastCellNode = cellLinkedList.Last;
cellLinkedList.RemoveLast();
lastCellNode.Value.gameObject.SetActive(false);
reuseQueue.Enqueue(lastCellNode.Value);
//상단에 새로운 Cell 생성
LinkedListNode<GOTableViewCell> firstCellNode = cellLinkedList.First;
int currentIndex = firstCellNode.Value.Index;
GOTableViewCell cell = cellForRowAtIndex(currentIndex - 1);
cell.gameObject.transform.localPosition = new Vector3(0, -(currentIndex - 1) * cellHeight, 0);
cellLinkedList.AddBefore(firstCellNode, cell);
cell.gameObject.transform.SetAsFirstSibling();
}
}
}
양 끝으로 가도 오류가 나지 않고 잘 작동한다. 굿~
Unity Version Control 유니티 버전 컨트롤 사용하기
3D URP 프로젝트 생성, Use Unity Version Control을 체크하고 location은 Japan으로 설정한다.
에셋 스토어에서 무료 에셋을 하나 받는다.
아래의 Plastic SCM 홈페이지 아래로 내려가서 [Download Plastic SCM] 버튼을 눌러 다운로드 한다.
[Window] - [Unity Version Control] 창을 켜고, 창에서 로그인, Create WorkSpace를 하면 다음과 같이 추가된 에셋들을 볼 수 있다.
Tanks 폴더 하나 만들고, Scenes 폴더를 제외한 모든 요소를 Tanks 폴더에 넣는다.
이미 체크가 되어있는 요소들을 [Check in Changes] 버튼으로 Check in 한다. github의 commit과 같다.